From 697ec52120f5bbf8a5a73b67544db7fba738c9e1 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 13 Jan 2023 18:10:35 +0200 Subject: [PATCH 01/14] Build 0.21.1.0 --- .../Characters/Animation/Ragdoll.cs | 21 +- .../ClientSource/Characters/Character.cs | 4 +- .../ClientSource/Characters/CharacterHUD.cs | 3 +- .../ClientSource/Characters/CharacterInfo.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 5 +- .../ClientSource/GUI/TabMenu.cs | 2 +- .../ClientSource/GUI/TalentMenu.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 256 +++++++++++++----- .../BarotraumaClient/ClientSource/GameMain.cs | 47 +--- .../GameSession/GameModes/CampaignMode.cs | 7 +- .../ClientSource/Items/CharacterInventory.cs | 2 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Items/Components/ItemContainer.cs | 79 ++++++ .../Items/Components/LightComponent.cs | 15 +- .../Items/Components/Machines/MiniMap.cs | 5 +- .../Items/Components/Signal/Wire.cs | 57 ++-- .../ClientSource/Items/Inventory.cs | 84 +----- .../ClientSource/Items/ItemPrefab.cs | 6 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 4 +- .../ClientSource/Map/MapEntity.cs | 4 +- .../ClientSource/Map/RoundSound.cs | 5 +- .../ClientSource/Map/SubmarinePreview.cs | 131 ++++++--- .../ClientSource/Networking/BanList.cs | 19 +- .../Networking/ChildServerRelay.cs | 1 + .../ClientSource/Networking/GameClient.cs | 101 ++++--- .../ClientEntityEventManager.cs | 19 +- .../Networking/Primitives/Peers/ClientPeer.cs | 2 + .../ClientSource/Networking/Voting.cs | 7 +- .../CampaignSetupUI/CampaignSetupUI.cs | 2 +- .../SinglePlayerCampaignSetupUI.cs | 2 +- .../CharacterEditor/CharacterEditorScreen.cs | 11 +- .../Screens/EventEditor/EventEditorScreen.cs | 9 + .../ClientSource/Screens/MainMenuScreen.cs | 88 ++++-- .../ClientSource/Screens/NetLobbyScreen.cs | 16 +- .../ClientSource/Screens/SubEditorScreen.cs | 21 +- .../ClientSource/Settings/SettingsMenu.cs | 2 + .../ClientSource/Sounds/SoundManager.cs | 6 + .../ClientSource/Sounds/SoundPlayer.cs | 13 +- .../ClientSource/Steam/WorkshopMenu/BBCode.cs | 35 ++- .../WorkshopMenu/Mutable/InstalledTab.cs | 2 +- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 2 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 14 + .../ClientSource/Upgrades/UpgradePrefab.cs | 2 +- .../Utils/LocalizationCSVtoXML.cs | 86 ++++-- .../ClientSource/Utils/SpriteRecorder.cs | 200 ++++++++------ .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../Characters/CharacterNetworking.cs | 45 ++- .../ServerSource/DebugConsole.cs | 2 +- .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../GameSession/GameModes/CampaignMode.cs | 7 +- .../GameModes/MultiPlayerCampaign.cs | 4 +- .../Items/Components/Projectile.cs | 2 +- .../ServerSource/Items/Item.cs | 10 +- .../ServerSource/Map/Submarine.cs | 7 +- .../ServerSource/Networking/GameServer.cs | 149 +++++----- .../ServerSource/Networking/RespawnManager.cs | 3 +- .../ServerSource/Networking/Voting.cs | 19 +- .../ServerSource/Screens/NetLobbyScreen.cs | 11 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 5 +- .../Characters/AI/AIController.cs | 15 + .../Characters/AI/EnemyAIController.cs | 13 +- .../Characters/AI/HumanAIController.cs | 67 +++-- .../AI/Objectives/AIObjectiveContainItem.cs | 2 +- .../Objectives/AIObjectiveFindDivingGear.cs | 45 ++- .../AI/Objectives/AIObjectiveFindSafety.cs | 2 +- .../AI/Objectives/AIObjectiveRescue.cs | 2 +- .../Characters/AI/Wreck/WreckAI.cs | 6 +- .../Characters/Animation/AnimController.cs | 8 +- .../Animation/FishAnimController.cs | 4 +- .../Animation/HumanoidAnimController.cs | 76 ++++-- .../Characters/Animation/Ragdoll.cs | 43 ++- .../SharedSource/Characters/Attack.cs | 90 ++++-- .../SharedSource/Characters/Character.cs | 128 +++++---- .../SharedSource/Characters/CharacterInfo.cs | 7 +- .../Health/Afflictions/AfflictionPrefab.cs | 12 + .../Characters/Health/CharacterHealth.cs | 20 +- .../SharedSource/Characters/Limb.cs | 86 ++++-- .../AbilityConditionAttackData.cs | 7 +- .../AbilityConditionCharacter.cs | 1 - .../AbilityConditionItem.cs | 31 ++- .../CharacterAbilityGiveItemStatToTags.cs | 8 + .../Abilities/CharacterAbilityModifyStat.cs | 5 + .../CharacterAbilityRemoveRandomIngredient.cs | 22 +- .../ContentPackage/ContentPackage.cs | 4 +- .../ContentPackageManager.cs | 15 +- .../ContentManagement/ContentXElement.cs | 1 + .../SharedSource/DebugConsole.cs | 8 +- .../BarotraumaShared/SharedSource/Enums.cs | 3 +- .../Missions/AbandonedOutpostMission.cs | 2 +- .../Events/Missions/BeaconMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 9 +- .../SharedSource/Events/MonsterEvent.cs | 2 +- .../GameAnalytics/GameAnalyticsManager.cs | 2 +- .../GameSession/GameModes/CampaignSettings.cs | 2 +- .../GameSession/GameModes/GameModePreset.cs | 16 +- .../GameSession/UpgradeManager.cs | 29 +- .../Items/Components/DockingPort.cs | 20 +- .../SharedSource/Items/Components/Door.cs | 8 +- .../Items/Components/ElectricalDischarger.cs | 3 +- .../Items/Components/Holdable/MeleeWeapon.cs | 111 +++++--- .../Items/Components/Holdable/RangedWeapon.cs | 2 + .../Items/Components/Holdable/RepairTool.cs | 31 ++- .../Items/Components/Holdable/Throwable.cs | 2 +- .../Items/Components/ItemComponent.cs | 8 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Projectile.cs | 28 +- .../SharedSource/Items/Components/Quality.cs | 10 +- .../SharedSource/Items/Components/Rope.cs | 7 +- .../Items/Components/Signal/LightComponent.cs | 31 ++- .../Items/Components/Signal/Wire.cs | 23 +- .../SharedSource/Items/Components/Turret.cs | 4 +- .../SharedSource/Items/Inventory.cs | 7 +- .../SharedSource/Items/Item.cs | 41 ++- .../SharedSource/Items/ItemEventData.cs | 2 +- .../SharedSource/Items/ItemPrefab.cs | 10 +- .../SharedSource/Map/Explosion.cs | 6 + .../SharedSource/Map/Levels/Level.cs | 24 +- .../SharedSource/Map/Map/Map.cs | 4 +- .../SharedSource/Map/MapEntityPrefab.cs | 3 + .../Map/Outposts/OutpostGenerator.cs | 13 +- .../SharedSource/Map/StructurePrefab.cs | 4 +- .../SharedSource/Map/Submarine.cs | 92 +++---- .../SharedSource/Map/SubmarineBody.cs | 6 +- .../SharedSource/Networking/Client.cs | 2 +- .../Networking/INetSerializable.cs | 2 +- .../SharedSource/Networking/NetworkMember.cs | 13 + .../Primitives/NetworkPeerStructs.cs | 2 + .../SharedSource/Networking/ServerSettings.cs | 2 +- .../SharedSource/Networking/Voting.cs | 4 +- .../Serialization/XMLExtensions.cs | 40 +++ .../SharedSource/Settings/GameSettings.cs | 2 - .../StatusEffects/PropertyConditional.cs | 1 + .../StatusEffects/StatusEffect.cs | 7 +- .../SharedSource/Steam/SteamManager.cs | 9 - .../SharedSource/Steam/Workshop.cs | 113 ++++++-- .../SharedSource/Upgrades/UpgradePrefab.cs | 113 +++++++- .../SharedSource/Utils/Md5Hash.cs | 7 +- .../SharedSource/Utils/Range.cs | 2 +- .../SharedSource/Utils/Result.cs | 4 +- .../SharedSource/Utils/SegmentTable.cs | 20 +- Barotrauma/BarotraumaShared/changelog.txt | 101 +++++++ ...tSerializableStructImplementationChecks.cs | 103 ++++--- .../INetSerializableStructTests.cs | 32 ++- .../Facepunch.Steamworks/Structs/UgcItem.cs | 16 ++ Libraries/Lidgren.Network/NetPeer.Internal.cs | 27 +- .../NetPeer.LatencySimulation.cs | 2 +- Libraries/Lidgren.Network/NetPeer.cs | 2 +- Libraries/Lidgren.Network/NetUtility.cs | 30 +- .../Graphics/SpriteBatch.cs | 25 +- 155 files changed, 2423 insertions(+), 1237 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 007c6d8a3..a9fa83ac4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -107,7 +107,7 @@ namespace Barotrauma Collider.AngularVelocity = newAngularVelocity; float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.CanMove ? 0.01f : 0.2f; + float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { if (distSqrd > 10.0f || !character.CanMove) @@ -145,6 +145,7 @@ namespace Barotrauma { MainLimb.PullJointWorldAnchorB = Collider.SimPosition; MainLimb.PullJointEnabled = true; + MainLimb.body.LinearVelocity = newVelocity; } } } @@ -442,10 +443,20 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; } - Vector2 spriteOrigin = limb.ActiveSprite.Origin; - spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; - limb.ActiveSprite.Origin = spriteOrigin; + if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; } + + FlipSprite(limb.DeformSprite?.Sprite ?? limb.Sprite); + foreach (var conditionalSprite in limb.ConditionalSprites) + { + FlipSprite(conditionalSprite.DeformableSprite?.Sprite ?? conditionalSprite.Sprite); + } + } + static void FlipSprite(Sprite sprite) + { + if (sprite == null) { return; } + Vector2 spriteOrigin = sprite.Origin; + spriteOrigin.X = sprite.SourceRect.Width - spriteOrigin.X; + sprite.Origin = spriteOrigin; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 770aa20e8..d20050fe4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -616,7 +616,7 @@ namespace Barotrauma return closestItem; } - private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f) + private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = MaxHighlightDistance) { Character closestCharacter = null; @@ -626,7 +626,7 @@ namespace Barotrauma { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } - float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); + float dist = c.GetDistanceToClosestLimb(mouseSimPos); if (dist < closestDist || (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index ff196c018..d0b25b398 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -608,7 +608,8 @@ namespace Barotrauma } } - Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) * 0.7f; + float dist = Vector2.Distance(character.FocusedCharacter.DrawPosition, character.DrawPosition); + Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) / dist * Math.Min(dist, Character.MaxDragDistance); startPos = cam.WorldToScreen(startPos); string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index b472749ff..c4bdcaba9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -938,7 +938,7 @@ namespace Barotrauma var headPreset = obj as HeadPreset; if (info.Head.Preset != headPreset) { - info.Head = new HeadInfo(info, headPreset) + info.Head = new HeadInfo(info, headPreset, info.Head.HairIndex, info.Head.BeardIndex, info.Head.MoustacheIndex, info.Head.FaceAttachmentIndex) { SkinColor = info.Head.SkinColor, HairColor = info.Head.HairColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index f39de7890..d9801606c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -640,8 +640,7 @@ namespace Barotrauma else { forceAfflictionContainerUpdate = true; - currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true) - .FindAll(a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); + currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); currentDisplayedAfflictions.Sort((a1, a2) => { int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); @@ -1275,7 +1274,7 @@ namespace Barotrauma //displaying an affliction we no longer have -> dirty foreach ((Affliction affliction, float strength) in displayedAfflictions) { - if (!afflictions.Any(a => a.Key == affliction)) { return true; } + if (afflictions.None(a => a.Key == affliction && a.Key.ShouldShowIcon(Character))) { return true; } } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 781483117..ee9de1b18 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1766,7 +1766,7 @@ namespace Barotrauma { foreach (UpgradePrefab prefab in categoryData.Prefabs) { - var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false); + var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false).Frame; UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 354ef4180..9b5318e30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -796,7 +796,7 @@ namespace Barotrauma CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo; if (ownCharacterInfo is null) { return false; } - return info == ownCharacterInfo; + return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName(); } public static bool CanManageTalents(CharacterInfo targetInfo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 445a77787..3399c5399 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -90,6 +89,16 @@ namespace Barotrauma Repairs } + private enum UpgradeStoreUserData + { + BuyButton, + BuyButtonLayout, + ProgressBarLayout, + IncreaseLabel, + PriceLabel, + MaterialCostList + } + public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) { WaitForServerUpdate = false; @@ -600,7 +609,7 @@ namespace Barotrauma GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); - GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; + GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = UpgradeStoreUserData.BuyButtonLayout }; new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); @@ -950,7 +959,7 @@ namespace Barotrauma frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, - 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton")); + 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame); if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton) { @@ -987,11 +996,11 @@ namespace Barotrauma int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, - price, replacement, - addBuyButton: true, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + price, replacement, + addBuyButton: true, addProgressBar: false, - buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); + buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton").Frame); if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } if (PlayerBalance >= price) @@ -1081,13 +1090,23 @@ namespace Barotrauma }; } - public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) + public readonly record struct BuyButtonFrame(GUILayoutGroup Layout, GUIListBox MaterialCostList, GUIButton BuyButton, GUITextBlock PriceText); + public readonly record struct ProgressBarFrame(GUITextBlock ProgressText, GUIProgressBar ProgressBar); + + public readonly record struct UpgradeFrame(GUIFrame Frame, + GUIImage Icon, + GUITextBlock Name, + GUITextBlock Description, + Option BuyButton, + Option ProgressBar); + + public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } - public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) + public static UpgradeFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) { float progressBarHeight = 0.25f; @@ -1105,21 +1124,26 @@ namespace Barotrauma * |------------------------------------------------------------------| */ GUIFrame prefabFrame = new GUIFrame(parent, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = userData }; - GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: true) { Stretch = true }; - GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); - var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; - GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; - GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); - var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; - GUILayoutGroup? progressLayout = null; + GUILayoutGroup mainLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: false); + GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(1f, addBuyButton ? 0.65f : 1f, mainLayout, Anchor.Center), isHorizontal: true) { Stretch = true }; + GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); + var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; + GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); + var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); + var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + GUILayoutGroup? progressLayout = null; GUILayoutGroup? buyButtonLayout = null; + Option buyButtonOption = Option.None(); + Option progressBarOption = Option.None(); + if (addProgressBar) { - progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; - new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); - new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = UpgradeStoreUserData.ProgressBarLayout }; + GUITextBlock progressText = new GUITextBlock(rectT(0.15f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + GUIProgressBar progressBar = new GUIProgressBar(rectT(0.85f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); + progressBarOption = Option.Some(new ProgressBarFrame(progressText, progressBar)); } if (addBuyButton) @@ -1127,12 +1151,33 @@ namespace Barotrauma var formattedPrice = TextManager.FormatCurrency(Math.Abs(price)); //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } - buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; - var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center) + buyButtonLayout = new GUILayoutGroup(rectT(1f, 0.35f, mainLayout), isHorizontal: true) { UserData = UpgradeStoreUserData.BuyButtonLayout };; + + GUIListBox materialCostList; + if (upgradePrefab is not null) { + var increaseText = new GUITextBlock(rectT(imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), "", textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) + { + UserData = UpgradeStoreUserData.IncreaseLabel + }; + UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel); + materialCostList = new GUIListBox(rectT(0.65f - imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), isHorizontal: true, style: null); + } + else + { + materialCostList = new GUIListBox(rectT(0.65f, 1f, buyButtonLayout), isHorizontal: true, style: null); + } + + materialCostList.Visible = false; + materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; + + var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice) + { + UserData = UpgradeStoreUserData.PriceLabel, //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades Visible = userData is ItemPrefab }; + if (price < 0) { priceText.TextColor = GUIStyle.Green; @@ -1141,15 +1186,13 @@ namespace Barotrauma { priceText.Text = string.Empty; } - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) + GUIButton buyButton = new GUIButton(rectT(0.15f, 1f, buyButtonLayout), string.Empty, style: buttonStyle) { + UserData = UpgradeStoreUserData.BuyButton, Enabled = false }; - if (upgradePrefab != null) - { - var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center); - UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel); - } + + buyButtonOption = Option.Some(new BuyButtonFrame(buyButtonLayout, materialCostList, buyButton, priceText)); } description.CalculateHeightFromText(); @@ -1175,7 +1218,7 @@ namespace Barotrauma progressLayout?.Recalculate(); buyButtonLayout?.Recalculate(); - return prefabFrame; + return new UpgradeFrame(prefabFrame, icon, name, description, buyButtonOption, progressBarOption); } private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel) @@ -1197,31 +1240,21 @@ namespace Barotrauma Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; if (Campaign is null || sub is null) { return; } - GUIFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.25f, parent)); - var prefabLayout = prefabFrame.GetChild(); - GUILayoutGroup[] childLayouts = prefabLayout.GetAllChildren().ToArray(); - var imageLayout = childLayouts[0]; - var icon = imageLayout.GetChild(); - var textLayout = childLayouts[1]; - var name = textLayout.GetChild(); - GUILayoutGroup[] textChildLayouts = textLayout.GetAllChildren().ToArray(); - var descriptionLayout = textChildLayouts[0]; - var description = descriptionLayout.GetChild(); - var progressLayout = textChildLayouts[1]; - var buyButtonLayout = childLayouts[2]; - var buyButton = buyButtonLayout.GetChild(); + UpgradeFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.4f, parent)); + + if (!prefabFrame.BuyButton.TryUnwrap(out BuyButtonFrame buyButtonFrame)) { return; } if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) { - prefabFrame.Enabled = false; - description.Enabled = false; - name.Enabled = false; - icon.Color = Color.Gray; - buyButton.Enabled = false; - buyButtonLayout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button + prefabFrame.Frame.Enabled = false; + prefabFrame.Description.Enabled = false; + prefabFrame.Name.Enabled = false; + prefabFrame.Icon.Color = Color.Gray; + buyButtonFrame.BuyButton.Enabled = false; + buyButtonFrame.Layout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button } - buyButton.OnClicked += (button, o) => + buyButtonFrame.BuyButton.OnClicked += (button, o) => { LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), @@ -1240,7 +1273,7 @@ namespace Barotrauma return true; }; - UpdateUpgradeEntry(prefabFrame, prefab, category, Campaign); + UpdateUpgradeEntry(prefabFrame.Frame, prefab, category, Campaign); } private void CreateItemTooltip(MapEntity entity) @@ -1623,7 +1656,7 @@ namespace Barotrauma int maxLevel = prefab.GetMaxLevelForCurrentSub(); LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString())); - if (prefabFrame.FindChild("progressbar", true) is { } progressParent) + if (prefabFrame.FindChild(UpgradeStoreUserData.ProgressBarLayout, true) is { } progressParent) { GUIProgressBar bar = progressParent.GetChild(); if (bar != null) @@ -1636,36 +1669,111 @@ namespace Barotrauma if (block != null) { block.Text = progressText; } } - if (prefabFrame.FindChild("buybutton", true) is { } buttonParent) + if (prefabFrame.FindChild(UpgradeStoreUserData.BuyButtonLayout, true) is not { } buttonParent) { return; } + + GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true); + priceLabel.Visible = true; + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + + if (!WaitForServerUpdate) { - List textBlocks = buttonParent.GetAllChildren().ToList(); - - GUITextBlock priceLabel = textBlocks[0]; - priceLabel.Visible = true; - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); - - if (priceLabel != null && !WaitForServerUpdate) + priceLabel.Text = TextManager.FormatCurrency(price); + if (currentLevel >= maxLevel) { - priceLabel.Text = TextManager.FormatCurrency(price); - if (currentLevel >= maxLevel) - { - priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); - } + priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); } + } - GUIButton button = buttonParent.GetChild(); - if (button != null) + if (buttonParent.FindChild(UpgradeStoreUserData.IncreaseLabel, recursive: true) is GUITextBlock increaseLabel && !WaitForServerUpdate) + { + UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel); + } + + bool isMax = currentLevel >= maxLevel; + + if (buttonParent.FindChild(UpgradeStoreUserData.BuyButton, recursive: true) is GUIButton button) + { + bool canBuy = !WaitForServerUpdate && !isMax && campaign.GetBalance() >= price && prefab.HasResourcesToUpgrade(Character.Controlled, currentLevel + 1); + + button.Enabled = canBuy; + } + + if (prefabFrame.FindChild(UpgradeStoreUserData.MaterialCostList, true) is GUIListBox itemList) + { + if (isMax) { - button.Enabled = currentLevel < maxLevel; - if (WaitForServerUpdate || campaign.GetBalance() < price) - { - button.Enabled = false; - } + itemList.Visible = false; } - GUITextBlock increaseLabel = textBlocks[1]; - if (increaseLabel != null && !WaitForServerUpdate) + else { - UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel); + CreateMaterialCosts(itemList, prefab, currentLevel + 1); + } + } + + static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel) + { + list.Content.ClearChildren(); + List allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List(); + + var resources = prefab.GetApplicableResources(targetLevel); + + foreach (ApplicableResourceCollection collection in resources) + { + list.Visible = true; + + int length = collection.MatchingItems.Length; + + if (length is 0) { continue; } + + ItemPrefab defaultItemPrefab = collection.MatchingItems.First(); + + GUILayoutGroup wrapperLayout = new GUILayoutGroup(rectT(0.25f, 1f, list.Content)); + + GUIFrame itemFrame = new GUIFrame(rectT(1f, 1f, wrapperLayout), style: null) + { + ToolTip = defaultItemPrefab.Name + }; + + bool hasItems = collection.Cost.Amount <= allItems.Count(collection.Cost.MatchesItem); + + Sprite icon = defaultItemPrefab.InventoryIcon ?? prefab.Sprite; + Color iconColor = defaultItemPrefab.InventoryIcon is null ? defaultItemPrefab.SpriteColor : defaultItemPrefab.InventoryIconColor; + + GUIImage itemIcon = new GUIImage(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), sprite: icon, scaleToFit: true) + { + Color = hasItems ? iconColor : iconColor * 0.9f, + CanBeFocused = false + }; + + // item count text + new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), itemIcon.RectTransform, anchor: Anchor.BottomRight), $"{collection.Count}", font: GUIStyle.Font, textAlignment: Alignment.BottomRight) + { + Shadow = true, + CanBeFocused = false, + Padding = Vector4.Zero, + TextColor = hasItems ? Color.White : GUIStyle.Red, + }; + + if (length is 1) { continue; } + + // we have more than 1 item, show a "slideshow" of the items + + float index = 0f; + GUICustomComponent customComponent = new GUICustomComponent(rectT(1f, 1f, itemFrame), null, (deltaTime, component) => + { + index += deltaTime / 3f; + if (index > length) { index = 0; } + + ItemPrefab currentPrefab = collection.MatchingItems[(int)MathF.Floor(index)]; + Sprite icon = currentPrefab.InventoryIcon ?? prefab.Sprite; + Color iconColor = currentPrefab.InventoryIcon is null ? currentPrefab.SpriteColor : currentPrefab.InventoryIconColor; + itemIcon.Sprite = icon; + itemIcon.Color = hasItems ? iconColor : iconColor * 0.9f; + itemFrame.ToolTip = currentPrefab.Name; + }) + { + CanBeFocused = false + }; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 8bc5fa434..2654387fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Reflection; using System.Threading; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -475,6 +476,19 @@ namespace Barotrauma yield return CoroutineStatus.Running; } + var corePackage = ContentPackageManager.EnabledPackages.Core; + if (corePackage.EnableError.TryUnwrap(out var error)) + { + if (error.ErrorsOrException.TryGet(out ImmutableArray errorMessages)) + { + throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {errorMessages.First()}"); + } + else if (error.ErrorsOrException.TryGet(out Exception exception)) + { + throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {exception.Message}", exception); + } + } + TextManager.VerifyLanguageAvailable(); DebugConsole.Init(); @@ -735,8 +749,8 @@ namespace Barotrauma { Client.Quit(); Client = null; - MainMenuScreen.Select(); } + MainMenuScreen.Select(); if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId)) { @@ -1099,37 +1113,6 @@ namespace Barotrauma GameSession = null; } - public void ShowEditorDisclaimer() - { - var msgBox = new GUIMessageBox(TextManager.Get("EditorDisclaimerTitle"), TextManager.Get("EditorDisclaimerText")); - var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; - linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); - List<(LocalizedString Caption, string Url)> links = new List<(LocalizedString, string)>() - { - (TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value), - (TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl").Fallback("https://discordapp.com/invite/undertow").Value), - }; - foreach (var link in links) - { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.Caption, style: "MainMenuGUIButton", textAlignment: Alignment.Left) - { - UserData = link.Url, - OnClicked = (btn, userdata) => - { - ShowOpenUrlInWebBrowserPrompt(userdata as string); - return true; - } - }; - } - - msgBox.InnerFrame.RectTransform.MinSize = new Point(0, - msgBox.InnerFrame.Rect.Height + linkHolder.Rect.Height + msgBox.Content.AbsoluteSpacing * 2 + 10); - var config = GameSettings.CurrentConfig; - config.EditorDisclaimerShown = true; - GameSettings.SetCurrentConfig(config); - GameSettings.SaveCurrentConfig(); - } - public void ShowBugReporter() { if (GUIMessageBox.VisibleBox != null && GUIMessageBox.VisibleBox.UserData as string == "bugreporter") diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 856417e04..3aae45f01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -106,12 +106,7 @@ namespace Barotrauma public static bool AllowedToManageWallets() { - if (GameMain.Client == null) { return true; } - - return - GameMain.Client.HasPermission(ClientPermissions.ManageMoney) || - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Client.IsServerOwner; + return AllowedToManageCampaign(ClientPermissions.ManageMoney); } public override void Draw(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 05a85c692..11530061e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -965,7 +965,7 @@ namespace Barotrauma break; case QuickUseAction.PutToEquippedItem: //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.OrderBy(it => it.ContainedItems.FirstOrDefault()?.Condition ?? 0.0f)) + foreach (Item heldItem in character.HeldItems.OrderBy(it => it.GetComponent()?.GetContainedIndicatorState() ?? 0.0f)) { if (heldItem.OwnInventory == null) { continue; } //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index fd10eb0f0..f50239f35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components public void DrawElectricity(SpriteBatch spriteBatch) { - if (timer <= 0.0f) { return; } + if (timer <= 0.0f && Screen.Selected is { IsEditor: false }) { return; } for (int i = 0; i < nodes.Count; i++) { if (nodes[i].Length <= 1.0f) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index d8c379c03..5b16c7d9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,7 +1,9 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections; using System.Linq; +using static Barotrauma.Inventory; namespace Barotrauma.Items.Components { @@ -250,6 +252,83 @@ namespace Barotrauma.Items.Components return true; } + + public float GetContainedIndicatorState() + { + if (ShowConditionInContainedStateIndicator) + { + return item.Condition / item.MaxCondition; + } + + int targetSlot = Math.Max(ContainedStateIndicatorSlot, 0); + if (targetSlot >= Inventory.Capacity) { return 0.0f; } + + var containedItems = Inventory.GetItemsAt(targetSlot); + if (containedItems == null) { return 0.0f; } + + Item containedItem = containedItems.FirstOrDefault(); + if (ShowTotalStackCapacityInContainedStateIndicator) + { + // No item on the defined slot, check if the items on other slots can be used. + containedItem ??= + containedItems.FirstOrDefault() ?? + Inventory.AllItems.FirstOrDefault(it => CanBeContained(it, targetSlot)); + if (containedItem == null) { return 0.0f; } + + int ignoredItemCount = 0; + var subContainableItems = AllSubContainableItems; + float capacity = GetMaxStackSize(targetSlot); + if (subContainableItems != null) + { + bool useMainContainerCapacity = true; + foreach (Item it in Inventory.AllItems) + { + // Ignore all items in the sub containers. + foreach (RelatedItem ri in subContainableItems) + { + if (ri.MatchesItem(containedItem)) + { + // The target item is in a subcontainer -> inverse the logic. + useMainContainerCapacity = false; + break; + } + if (ri.MatchesItem(it)) + { + ignoredItemCount++; + } + } + if (!useMainContainerCapacity) { break; } + } + if (useMainContainerCapacity) + { + capacity *= MainContainerCapacity; + } + else + { + // Ignore all items in the main container. + ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); + capacity *= Capacity - MainContainerCapacity; + } + } + int itemCount = Inventory.AllItems.Count() - ignoredItemCount; + return Math.Min(itemCount / Math.Max(capacity, 1), 1); + } + else + { + if (containedItem != null && (Inventory.Capacity == 1 || HasSubContainers)) + { + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot)); + if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) + { + return containedItems.Count() / (float)maxStackSize; + } + } + return Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1 ? + (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : + Inventory.EmptySlotCount / (float)Inventory.Capacity; + } + } + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 1d4db25d2..b8d40669d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -14,8 +14,6 @@ namespace Barotrauma.Items.Components private CoroutineHandle resetPredictionCoroutine; private float resetPredictionTimer; - private float currentBrightness; - public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -29,14 +27,21 @@ namespace Barotrauma.Items.Components Light.Position = ParentBody != null ? ParentBody.Position : item.Position; } - partial void SetLightSourceState(bool enabled, float brightness) + partial void SetLightSourceState(bool enabled, float? brightness) { if (Light == null) { return; } Light.Enabled = enabled; - currentBrightness = brightness; + if (brightness.HasValue) + { + lightBrightness = brightness.Value; + } + else + { + lightBrightness = enabled ? 1.0f : 0.0f; + } if (enabled) { - Light.Color = LightColor.Multiply(brightness); + Light.Color = LightColor.Multiply(lightBrightness); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 973f6e514..85ac7fd01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -519,7 +519,10 @@ namespace Barotrauma.Items.Components Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green; weaponSprite.Draw(batch, center, color, origin, rotation, scale, SpriteEffects.None); } - }); + }) + { + CanBeFocused = false + }; weaponChilds.Add(component, frame); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index c97a2945e..6ec91dabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -25,14 +25,14 @@ namespace Barotrauma.Items.Components public static Color editorHighlightColor = Color.Yellow; public static Color editorSelectedColor = Color.Red; - partial class WireSection + public partial class WireSection { public VertexPositionColorTexture[] vertices; public VertexPositionColorTexture[] shiftedVertices; private float cachedWidth = 0f; - private void RecalculateVertices(Wire wire, float width) + private void RecalculateVertices(Sprite wireSprite, float width) { if (MathUtils.NearlyEqual(cachedWidth, width)) { return; } cachedWidth = width; @@ -45,13 +45,13 @@ namespace Barotrauma.Items.Components expandDir.X = -expandDir.Y; expandDir.Y = -temp; - Rectangle srcRect = wire.wireSprite.SourceRect; + Rectangle srcRect = wireSprite.SourceRect; expandDir *= width * srcRect.Height * 0.5f; Vector2 rectLocation = srcRect.Location.ToVector2(); Vector2 rectSize = srcRect.Size.ToVector2(); - Vector2 textureSize = new Vector2(wire.wireSprite.Texture.Width, wire.wireSprite.Texture.Height); + Vector2 textureSize = new Vector2(wireSprite.Texture.Width, wireSprite.Texture.Height); Vector2 topLeftUv = rectLocation / textureSize; Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize; @@ -67,10 +67,10 @@ namespace Barotrauma.Items.Components shiftedVertices = (VertexPositionColorTexture[])vertices.Clone(); } - public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) + public void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Color color, Vector2 offset, float depth, float width = 0.3f) { if (width <= 0f) { return; } - RecalculateVertices(wire, width); + RecalculateVertices(wireSprite, width); for (int i = 0; i < vertices.Length; i++) { @@ -79,21 +79,22 @@ namespace Barotrauma.Items.Components shiftedVertices[i].Position.X += offset.X; shiftedVertices[i].Position.Y -= offset.Y; } - spriteBatch.Draw(wire.wireSprite.Texture, + spriteBatch.Draw( + wireSprite.Texture, shiftedVertices, depth); } - public static void Draw(SpriteBatch spriteBatch, Wire wire, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) + public static void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) { start.Y = -start.Y; end.Y = -end.Y; - spriteBatch.Draw(wire.wireSprite.Texture, - start, wire.wireSprite.SourceRect, color, + spriteBatch.Draw(wireSprite.Texture, + start, wireSprite.SourceRect, color, MathUtils.VectorToAngle(end - start), - new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), - new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.size.X, width), + new Vector2(0.0f, wireSprite.size.Y / 2.0f), + new Vector2((Vector2.Distance(start, end)) / wireSprite.size.X, width), SpriteEffects.None, depth); } @@ -123,7 +124,7 @@ namespace Barotrauma.Items.Components get => draggingWire; } - partial void InitProjSpecific(ContentXElement element) + public static Sprite ExtractWireSprite(ContentXElement element) { if (defaultWireSprite == null) { @@ -133,6 +134,7 @@ namespace Barotrauma.Items.Components }; } + Sprite overrideSprite = null; foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) @@ -142,9 +144,14 @@ namespace Barotrauma.Items.Components } } - wireSprite = overrideSprite ?? defaultWireSprite; + return overrideSprite ?? defaultWireSprite; + } + + partial void InitProjSpecific(ContentXElement element) + { + wireSprite = ExtractWireSprite(element); + if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { @@ -181,20 +188,20 @@ namespace Barotrauma.Items.Components { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } else if (item.IsSelected) { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, item.Color, drawOffset, depth, Width); + section.Draw(spriteBatch, wireSprite, item.Color, drawOffset, depth, Width); } if (nodes.Count > 0) @@ -239,13 +246,13 @@ namespace Barotrauma.Items.Components } WireSection.Draw( - spriteBatch, this, - new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, + spriteBatch, wireSprite, + nodes[^1] + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.Color, 0.0f, Width); WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, item.Color, itemDepth, Width); @@ -255,8 +262,8 @@ namespace Barotrauma.Items.Components else { WireSection.Draw( - spriteBatch, this, - new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, + spriteBatch, wireSprite, + nodes[^1] + drawOffset, item.DrawPosition, item.Color, 0.0f, Width); } @@ -294,12 +301,12 @@ namespace Barotrauma.Items.Components Vector2 endPos = start + new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)) * 50.0f; WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, start, endPos, GUIStyle.Orange, depth + 0.00001f, 0.2f); WireSection.Draw( - spriteBatch, this, + spriteBatch, wireSprite, start, start + (endPos - start) * 0.7f, item.Color, depth, 0.3f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index a11245fe4..8728ef139 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1603,88 +1603,7 @@ namespace Barotrauma if (itemContainer != null && itemContainer.ShowContainedStateIndicator && itemContainer.Capacity > 0) { - float containedState = 0.0f; - if (itemContainer.ShowConditionInContainedStateIndicator) - { - containedState = item.Condition / item.MaxCondition; - } - else - { - int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0); - ItemSlot containedItemSlot = null; - if (targetSlot < itemContainer.Inventory.slots.Length) - { - containedItemSlot = itemContainer.Inventory.slots[targetSlot]; - } - if (containedItemSlot != null) - { - Item containedItem = containedItemSlot.FirstOrDefault(); - if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) - { - if (containedItem == null) - { - // No item on the defined slot, check if the items on other slots can be used. - containedItem = containedItemSlot.FirstOrDefault() ?? itemContainer.Inventory.AllItems.FirstOrDefault(it => itemContainer.CanBeContained(it, targetSlot)); - } - if (containedItem != null) - { - int ignoredItemCount = 0; - var subContainableItems = itemContainer.AllSubContainableItems; - float capacity = itemContainer.GetMaxStackSize(targetSlot); - if (subContainableItems != null) - { - bool useMainContainerCapacity = true; - foreach (Item it in itemContainer.Inventory.AllItems) - { - // Ignore all items in the sub containers. - foreach (RelatedItem ri in subContainableItems) - { - if (ri.MatchesItem(containedItem)) - { - // The target item is in a subcontainer -> inverse the logic. - useMainContainerCapacity = false; - break; - } - if (ri.MatchesItem(it)) - { - ignoredItemCount++; - } - } - if (!useMainContainerCapacity) { break; } - } - if (useMainContainerCapacity) - { - capacity *= itemContainer.MainContainerCapacity; - } - else - { - // Ignore all items in the main container. - ignoredItemCount = itemContainer.Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); - capacity *= itemContainer.Capacity - itemContainer.MainContainerCapacity; - } - } - int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItemCount; - containedState = Math.Min(itemCount / Math.Max(capacity, 1), 1); - } - } - else - { - containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? - (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : - itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; - - if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) - { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot)); - if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) - { - containedState = containedItemSlot.Items.Count / (float)maxStackSize; - } - } - } - } - } - + float containedState = itemContainer.GetContainedIndicatorState(); int dir = slot.SubInventoryDir; Rectangle containedIndicatorArea = new Rectangle(rect.X, dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); @@ -1807,6 +1726,7 @@ namespace Barotrauma } } + private static void DrawItemStateIndicator( SpriteBatch spriteBatch, Inventory inventory, Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 647e2dbc3..ee6bae0c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -260,16 +260,16 @@ namespace Barotrauma public override void UpdatePlacing(Camera cam) { - Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); if (PlayerInput.SecondaryMouseButtonClicked()) { Selected = null; return; } + + var potentialContainer = MapEntity.GetPotentialContainer(cam.ScreenToWorld(PlayerInput.MousePosition)); - var potentialContainer = MapEntity.GetPotentialContainer(position); - + Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); if (!ResizeHorizontal && !ResizeVertical) { if (PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 7024d24bb..095195834 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -293,11 +293,11 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(drawRect.X, -drawRect.Y), new Vector2(rect.Width, rect.Height), - Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f)); GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), - GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f)); if (GameMain.DebugDraw) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 2ebbfc897..51fa11094 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -515,11 +515,11 @@ namespace Barotrauma Item targetContainer = null; bool isShiftDown = PlayerInput.IsShiftDown(); - if (!isShiftDown) return null; + if (!isShiftDown) { return null; } foreach (MapEntity e in mapEntityList) { - if (!e.SelectableInEditor ||!(e is Item potentialContainer)) { continue; } + if (!e.SelectableInEditor || e is not Item potentialContainer) { continue; } if (e.IsMouseOn(position)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 365db83ac..c229c7011 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -83,7 +83,10 @@ namespace Barotrauma { string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + if (!ContentPackageManager.ModsEnabled) + { + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + } return null; } catch (System.IO.InvalidDataException e) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index e547ec854..a4087003a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -4,26 +4,29 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - class SubmarinePreview : IDisposable + sealed class SubmarinePreview : IDisposable { - private SpriteRecorder spriteRecorder; private readonly SubmarineInfo submarineInfo; + + private SpriteRecorder spriteRecorder; private Camera camera; private Task loadTask; + private (Vector2 Min, Vector2 Max) bounds; + private volatile bool isDisposed; private GUIFrame previewFrame; - private class HullCollection + private sealed class HullCollection { public readonly List Rects; public readonly LocalizedString Name; @@ -186,7 +189,21 @@ namespace Barotrauma }); recalculateSpecsContainerHeight(); - GeneratePreviewMeshes(); + TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ => + { + // Reset the camera's position on the main thread, + // because the Camera class is not thread-safe and + // it's possible for its state to not get updated + // properly if done within a task + camera.Position = (bounds.Min + bounds.Max) * (0.5f, -0.5f); + Vector2 span2d = bounds.Max - bounds.Min; + Vector2 scaledSpan2d = span2d / camera.Resolution.ToVector2(); + float scaledSpan = Math.Max(scaledSpan2d.X, scaledSpan2d.Y); + camera.MinZoom = Math.Min(0.1f, 0.4f / scaledSpan); + camera.Zoom = 0.7f / scaledSpan; + camera.StopMovement(); + camera.UpdateTransform(interpolate: false, updateListener: false); + }); } public static void AddToGUIUpdateList() @@ -207,6 +224,7 @@ namespace Barotrauma spriteRecorder.Begin(SpriteSortMode.BackToFront); HashSet toIgnore = new HashSet(); + HashSet wires = new HashSet(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { @@ -221,7 +239,7 @@ namespace Barotrauma ExtractItemContainerIds(component, toIgnore); break; case "connectionpanel": - ExtractConnectionPanelLinks(component, toIgnore); + ExtractConnectionPanelLinks(component, wires); break; } } @@ -231,20 +249,25 @@ namespace Barotrauma await Task.Yield(); } + var wireNodes = new List(); + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } switch (subElement.Name.LocalName.ToLowerInvariant()) { + case "structure": case "item": - if (!toIgnore.Contains(subElement.GetAttributeInt("ID", 0))) + var id = subElement.GetAttributeInt("ID", 0); + if (wires.Contains(id)) + { + wireNodes.Add(subElement); + } + else if (!toIgnore.Contains(id)) { BakeMapEntity(subElement); } break; - case "structure": - BakeMapEntity(subElement); - break; case "hull": Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); if (!identifier.IsEmpty) @@ -261,15 +284,14 @@ namespace Barotrauma if (isDisposed) { return; } await Task.Yield(); } - spriteRecorder.End(); - camera.Position = (spriteRecorder.Min + spriteRecorder.Max) * 0.5f; - float scaledSpan = (spriteRecorder.Max - spriteRecorder.Min).X / camera.Resolution.X; - camera.Zoom = 0.8f / scaledSpan; - camera.StopMovement(); + bounds = (spriteRecorder.Min, spriteRecorder.Max); + wireNodes.ForEach(BakeWireNodes); + + spriteRecorder.End(); } - private void ExtractItemContainerIds(XElement component, HashSet ids) + private static void ExtractItemContainerIds(XElement component, HashSet ids) { string containedString = component.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); @@ -283,7 +305,7 @@ namespace Barotrauma } } - private void ExtractConnectionPanelLinks(XElement component, HashSet ids) + private static void ExtractConnectionPanelLinks(XElement component, HashSet ids) { var pins = component.Elements("input").Concat(component.Elements("output")); foreach (var pin in pins) @@ -297,6 +319,39 @@ namespace Barotrauma } } + private void BakeWireNodes(XElement element) + { + var prefabIdentifier = element.GetAttributeIdentifier("identifier", ""); + if (prefabIdentifier.IsEmpty) { return; } + if (!ItemPrefab.Prefabs.TryGet(prefabIdentifier, out var prefab)) { return; } + + var prefabWireComponentElement = prefab.ConfigElement.GetChildElement("wire"); + if (prefabWireComponentElement is null) { return; } + + var wireComponent = element.GetChildElement("wire"); + if (wireComponent is null) { return; } + + var color = element.GetAttributeColor("spritecolor") ?? Color.White; + + var nodes = Wire.ExtractNodes(wireComponent).ToImmutableArray(); + var wireSprite = Wire.ExtractWireSprite(prefab.ConfigElement); + + var useSpriteDepth = element.GetAttributeBool("usespritedepth", false); + var depth = + useSpriteDepth + ? element.GetAttributeFloat("spritedepth", 1.0f) + : wireSprite.Depth; + + var width = prefabWireComponentElement.GetAttributeFloat("width", 0.3f); + + for (int i = 0; i < nodes.Length - 1; i++) + { + var line = (Start: nodes[i], End: nodes[i + 1]); + var wireSegment = new Wire.WireSection(line.Start, line.End); + wireSegment.Draw(spriteRecorder, wireSprite, color, Vector2.Zero, depth, width); + } + } + private void BakeMapEntity(XElement element) { Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); @@ -313,27 +368,27 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = null; - if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && - ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) + MapEntityPrefab prefab; + if (element.NameAsIdentifier() == "item" + && ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) { prefab = ip; } else { - prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + prefab = MapEntityPrefab.FindByIdentifier(identifier); } if (prefab == null) { return; } - var texture = prefab.Sprite.Texture; - var srcRect = prefab.Sprite.SourceRect; + flippedX &= prefab.CanSpriteFlipX; + flippedY &= prefab.CanSpriteFlipY; SpriteEffects spriteEffects = SpriteEffects.None; - if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) + if (flippedX) { spriteEffects |= SpriteEffects.FlipHorizontally; } - if (flippedY && ((prefab as ItemPrefab)?.CanSpriteFlipY ?? true)) + if (flippedY) { spriteEffects |= SpriteEffects.FlipVertically; } @@ -419,8 +474,8 @@ namespace Barotrauma { float offsetState = 0f; Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; - if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + if (flippedX) { offset.X = -offset.X; } + if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteRecorder, new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), rect.Size.ToVector2(), color: color, @@ -451,8 +506,8 @@ namespace Barotrauma float rotationState = 0f; float offsetState = 0f; float rot = decorativeSprite.GetRotation(ref rotationState, 0f); Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; - if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + 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, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); @@ -472,6 +527,7 @@ namespace Barotrauma { overrideSprite = false; + float relativeScale = scale / prefab.Scale; foreach (var subElement in prefab.ConfigElement.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) @@ -498,7 +554,6 @@ namespace Barotrauma relativeBarrelPos, MathHelper.ToRadians(rotation)); - float relativeScale = scale / prefab.Scale; 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; @@ -516,20 +571,22 @@ namespace Barotrauma break; case "door": - doors.Add(new Door(rect)); + var scaledRect = rect with { Size = (rect.Size.ToVector2() * relativeScale).ToPoint() }; + + doors.Add(new Door(scaledRect)); var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); if (doorSpriteElem != null) { - string texturePath = doorSpriteElem.GetAttributeString("texture", ""); - Vector2 pos = rect.Location.ToVector2() * new Vector2(1f, -1f); + string texturePath = doorSpriteElem.GetAttributeStringUnrestricted("texture", ""); + Vector2 pos = scaledRect.Location.ToVector2() * new Vector2(1f, -1f); if (subElement.GetAttributeBool("horizontal", false)) { - pos.Y += (float)rect.Height * 0.5f; + pos.Y += (float)scaledRect.Height * 0.5f; } else { - pos.X += (float)rect.Width * 0.5f; + pos.X += (float)scaledRect.Width * 0.5f; } Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); spriteRecorder.Draw(doorSprite.Texture, pos, @@ -555,7 +612,7 @@ namespace Barotrauma } } - public void ParseUpgrades(XElement prefabConfigElement, ref float scale) + private void ParseUpgrades(XElement prefabConfigElement, ref float scale) { foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 94e3da6c4..68ba8fbae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -66,9 +66,20 @@ namespace Barotrauma.Networking }; var addressOrAccountId = bannedPlayer.AddressOrAccountId; - GUITextBlock textBlock = new GUITextBlock( - new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), - bannedPlayer.Name + " (" + addressOrAccountId + ")") { CanBeFocused = true }; + + string nameText = bannedPlayer.Name; + if (addressOrAccountId.TryCast(out Address address)) + { + nameText += $" ({address.StringRepresentation})"; + } + else if (addressOrAccountId.TryCast(out AccountId accountId)) + { + nameText += $" ({accountId.StringRepresentation})"; + } + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), nameText) + { + CanBeFocused = true + }; textBlock.RectTransform.MinSize = new Point( (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0); @@ -106,7 +117,7 @@ namespace Barotrauma.Networking private bool RemoveBan(GUIButton button, object obj) { - if (!(obj is BannedPlayer banned)) { return false; } + if (obj is not BannedPlayer banned) { return false; } localRemovedBans.Add(banned.UniqueIdentifier); RecreateBanFrame(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index 89abf5bf8..61bf8cec3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -34,6 +34,7 @@ namespace Barotrauma.Networking catch { DebugConsole.ThrowError($"Failed to start ChildServerRelay Process. File: {processInfo.FileName}, arguments: {processInfo.Arguments}"); + ForceShutDown(); throw; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 98fd63aa1..34449232a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -522,7 +522,7 @@ namespace Barotrauma.Networking if (GameStarted && Screen.Selected == GameMain.GameScreen) { - EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); + EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned; RespawnManager?.Update(deltaTime); @@ -1102,11 +1102,7 @@ namespace Barotrauma.Networking VoipClient = new VoipClient(this, ClientPeer); //if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods - if (!(Screen.Selected is GameScreen) && !(Screen.Selected is RoundSummaryScreen) && !(Screen.Selected is NetLobbyScreen)) - { - GameMain.ModDownloadScreen.Select(); - } - else + if (Screen.Selected is GameScreen or RoundSummaryScreen or NetLobbyScreen) { EntityEventManager.ClearSelf(); foreach (Character c in Character.CharacterList) @@ -1114,6 +1110,10 @@ namespace Barotrauma.Networking c.ResetNetState(); } } + else + { + GameMain.ModDownloadScreen.Select(); + } chatBox.InputBox.Enabled = true; if (GameMain.NetLobbyScreen?.ChatInput != null) @@ -1535,8 +1535,9 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; - DateTime? timeOut = null; + //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message TimeSpan timeOutDuration = new TimeSpan(0, 0, seconds: 30); + DateTime timeOut = DateTime.Now + timeOutDuration; DateTime requestFinalizeTime = DateTime.Now; TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); IWriteMessage msg = new WriteOnlyMessage(); @@ -1545,11 +1546,15 @@ namespace Barotrauma.Networking GUIMessageBox interruptPrompt = null; - while (true) + if (includesFinalize) { - try + ReadStartGameFinalize(inc); + } + else + { + while (true) { - if (timeOut.HasValue) + try { if (DateTime.Now > requestFinalizeTime) { @@ -1583,41 +1588,30 @@ namespace Barotrauma.Networking return true; }; } - } - else - { - if (includesFinalize) + + if (!connected) { - ReadStartGameFinalize(inc); + roundInitStatus = RoundInitStatus.Interrupted; break; } - //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message - timeOut = DateTime.Now + timeOutDuration; + if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } } - - if (!connected) + catch (Exception e) { - roundInitStatus = RoundInitStatus.Interrupted; + DebugConsole.ThrowError("There was an error initializing the round.", e, true); + roundInitStatus = RoundInitStatus.Error; break; } - if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } + //waiting for a STARTGAMEFINALIZE message + yield return CoroutineStatus.Running; } - catch (Exception e) - { - DebugConsole.ThrowError("There was an error initializing the round.", e, true); - roundInitStatus = RoundInitStatus.Error; - break; - } - - //waiting for a STARTGAMEFINALIZE message - yield return CoroutineStatus.Running; } interruptPrompt?.Close(); interruptPrompt = null; - + if (roundInitStatus != RoundInitStatus.Started) { if (roundInitStatus != RoundInitStatus.Interrupted) @@ -1767,7 +1761,7 @@ namespace Barotrauma.Networking { string subName = inc.ReadString(); string subHash = inc.ReadString(); - byte subClass = inc.ReadByte(); + SubmarineClass subClass = (SubmarineClass)inc.ReadByte(); bool isShuttle = inc.ReadBoolean(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); @@ -1776,7 +1770,7 @@ namespace Barotrauma.Networking { matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false) { - SubmarineClass = (SubmarineClass)subClass + SubmarineClass = subClass }; if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); } } @@ -2009,10 +2003,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); GameMain.NetLobbyScreen.SetMissionType(missionType); - if (!allowModeVoting) GameMain.NetLobbyScreen.SelectMode(modeIndex); + GameMain.NetLobbyScreen.SelectMode(modeIndex); if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); + if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); } } if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) @@ -2101,13 +2095,12 @@ namespace Barotrauma.Networking case ServerNetSegment.EntityPosition: inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); - UInt32 incomingUintIdentifier = inc.ReadUInt32(); - UInt16 id = inc.ReadUInt16(); uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - - var entity = Entity.FindEntityByID(id) as IServerPositionSync; + + var header = INetSerializableStruct.Read(inc); + + var entity = Entity.FindEntityByID(header.EntityId) as IServerPositionSync; if (msgEndPos > inc.LengthBits) { DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); @@ -2117,15 +2110,15 @@ namespace Barotrauma.Networking debugEntityList.Add(entity); if (entity != null) { - if (entity is Item != isItem) + if (entity is Item != header.IsItem) { - DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(header.IsItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); } - else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && - uintIdentifier != incomingUintIdentifier) + else if (entity is MapEntity { Prefab.UintIdentifier: var uintIdentifier } me && + uintIdentifier != header.PrefabUintIdentifier) { DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." - +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " + +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == header.PrefabUintIdentifier)?.Identifier.Value ?? "[not found]"}, " +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); } else @@ -2133,7 +2126,6 @@ namespace Barotrauma.Networking entity.ClientReadPosition(inc, sendingTime); } } - //force to the correct position in case the entity doesn't exist //or the message wasn't read correctly for whatever reason inc.BitPosition = msgEndPos; @@ -2144,7 +2136,7 @@ namespace Barotrauma.Networking break; case ServerNetSegment.EntityEvent: case ServerNetSegment.EntityEventInitial: - if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList)) + if (!EntityEventManager.Read(segment, inc, sendingTime)) { return SegmentTableReader.BreakSegmentReading.Yes; } @@ -2413,7 +2405,9 @@ namespace Barotrauma.Networking var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation).ToList(); + var existingSubs = SubmarineInfo.SavedSubmarines + .Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash) + .ToList(); foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); @@ -2472,12 +2466,13 @@ namespace Barotrauma.Networking } // Replace a submarine dud with the downloaded version - SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.StringRepresentation == newSub.MD5Hash?.StringRepresentation); + SubmarineInfo existingServerSub = ServerSubmarines.Find(s => + s.Name == newSub.Name + && s.MD5Hash == newSub.MD5Hash); if (existingServerSub != null) { int existingIndex = ServerSubmarines.IndexOf(existingServerSub); - ServerSubmarines.RemoveAt(existingIndex); - ServerSubmarines.Insert(existingIndex, newSub); + ServerSubmarines[existingIndex] = newSub; existingServerSub.Dispose(); } @@ -2798,7 +2793,6 @@ namespace Barotrauma.Networking /// public void RequestSelectMode(int modeIndex) { - if (!HasPermission(ClientPermissions.SelectMode)) return; if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren) { DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); @@ -2852,13 +2846,14 @@ namespace Barotrauma.Networking /// /// Tell the server to end the round (permission required) /// - public void RequestRoundEnd(bool save) + public void RequestRoundEnd(bool save, bool quitCampaign = false) { IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.ManageRound); msg.WriteBoolean(true); //indicates round end msg.WriteBoolean(save); + msg.WriteBoolean(quitCampaign); ClientPeer.Send(msg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 0d14de93b..3db7e69c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -109,16 +109,15 @@ namespace Barotrauma.Networking private UInt16? firstNewID; + private readonly List tempEntityList = new List(); /// /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// - public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List entities) + public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) { - UInt16 unreceivedEntityEventCount = 0; - if (type == ServerNetSegment.EntityEventInitial) { - unreceivedEntityEventCount = msg.ReadUInt16(); + UInt16 unreceivedEntityEventCount = msg.ReadUInt16(); firstNewID = msg.ReadUInt16(); if (GameSettings.CurrentConfig.VerboseLogging) @@ -143,7 +142,7 @@ namespace Barotrauma.Networking } } - entities.Clear(); + tempEntityList.Clear(); msg.ReadPadBits(); UInt16 firstEventID = msg.ReadUInt16(); @@ -156,9 +155,9 @@ namespace Barotrauma.Networking { string errorMsg = $"Error while reading a message from the server. Entity event data exceeds the size of the buffer (current position: {msg.BitPosition}, length: {msg.LengthBits})."; errorMsg += "\nPrevious entities:"; - for (int j = entities.Count - 1; j >= 0; j--) + for (int j = tempEntityList.Count - 1; j >= 0; j--) { - errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); + errorMsg += "\n" + (tempEntityList[j] == null ? "NULL" : tempEntityList[j].ToString()); } DebugConsole.ThrowError(errorMsg); return false; @@ -174,7 +173,7 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", Microsoft.Xna.Framework.Color.Orange); } - entities.Add(null); + tempEntityList.Add(null); if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } continue; } @@ -182,7 +181,7 @@ namespace Barotrauma.Networking int msgLength = (int)msg.ReadVariableUInt32(); IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable; - entities.Add(entity); + tempEntityList.Add(entity); //skip the event if we've already received it or if the entity isn't found if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null) @@ -223,7 +222,7 @@ namespace Barotrauma.Networking if (msg.BitPosition != msgPosition + msgLength * 8) { - var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; + var prevEntity = tempEntityList.Count >= 2 ? tempEntityList[tempEntityList.Count - 2] : null; ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 2e5976f61..54a932e0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -30,6 +30,8 @@ namespace Barotrauma.Networking protected readonly bool isOwner; protected readonly Option ownerKey; + public bool IsActive => isActive; + protected bool isActive; public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option ownerKey) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index f07d6f7a9..67ba366d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -98,16 +98,15 @@ namespace Barotrauma foreach (GUIComponent comp in listBox.Content.Children) { if (comp.UserData != userData) { continue; } - if (!(comp.FindChild("votes") is GUITextBlock voteText)) + if (comp.FindChild("votes") is not GUITextBlock voteText) { - voteText = new GUITextBlock(new RectTransform(new Point(30, comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), - "", textAlignment: Alignment.CenterRight) + voteText = new GUITextBlock(new RectTransform(new Point(GUI.IntScale(30), comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), + "", textAlignment: Alignment.Center) { Padding = Vector4.Zero, UserData = "votes" }; } - voteText.Text = votes == 0 ? "" : votes.ToString(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 5083a49c3..60dc7f6ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -303,7 +303,7 @@ namespace Barotrauma bool ChangeValue(GUIButton btn, object userData) { - if (!(userData is int change)) { return false; } + if (userData is not int change) { return false; } int hiddenOptions = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 1b8f74e3c..c13194e4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -365,7 +365,7 @@ namespace Barotrauma private void CreateCustomizeWindow(CampaignSettings prevSettings, Action onClosed = null) { - CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.3f), minSize: new Point(450, 350)); + CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.5f), minSize: new Point(450, 350)); GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 6eb0b00ab..78ef8b08a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -125,7 +125,7 @@ namespace Barotrauma.CharacterEditor { ResetVariables(); var subInfo = new SubmarineInfo("Content/AnimEditor.sub"); - Submarine.MainSub = new Submarine(subInfo); + Submarine.MainSub = new Submarine(subInfo, showErrorMessages: false); if (Submarine.MainSub.PhysicsBody != null) { Submarine.MainSub.PhysicsBody.Enabled = false; @@ -162,11 +162,6 @@ namespace Barotrauma.CharacterEditor OpenDoors(); GameMain.Instance.ResolutionChanged += OnResolutionChanged; Instance = this; - - if (!GameSettings.CurrentConfig.EditorDisclaimerShown) - { - GameMain.Instance.ShowEditorDisclaimer(); - } } private void ResetVariables() @@ -2688,10 +2683,6 @@ namespace Barotrauma.CharacterEditor // Character selection var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); - var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.2f, 0.7f), characterLabel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton") - { - OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; } - }; var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 620f10795..aee1762ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -820,6 +820,15 @@ namespace Barotrauma }; valueInput.Text = newValue?.ToString() ?? ""; } + else if (type == typeof(Identifier)) + { + GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? string.Empty); + valueInput.OnTextChanged += (component, o) => + { + newValue = new Identifier(o); + return true; + }; + } else if (type == typeof(float) || type == typeof(int)) { GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 799ea9c56..284a9634a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -51,9 +51,8 @@ namespace Barotrauma private readonly GUIFrame modsButtonContainer; private readonly GUIButton modsButton, modUpdatesButton; - private Task> modUpdateTask; - private float modUpdateTimer = 0.0f; - private const float ModUpdateInterval = 60.0f; + private (DateTime WhenToRefresh, int Count) modUpdateStatus = (DateTime.Now, 0); + private static readonly TimeSpan ModUpdateInterval = TimeSpan.FromSeconds(60.0f); private readonly GameMain game; @@ -736,8 +735,7 @@ namespace Barotrauma public void ResetModUpdateButton() { - modUpdateTask = null; - modUpdateTimer = 0; + modUpdateStatus = (DateTime.Now, 0); modUpdatesButton.Visible = false; } @@ -875,7 +873,25 @@ namespace Barotrauma GameMain.ResetNetLobbyScreen(); try { - string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; + string fileName; + if (serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f && + f.ContentPackage != GameMain.VanillaContent) + { + fileName = Path.Combine( + Path.GetDirectoryName(f.Path.Value), + Path.GetFileNameWithoutExtension(f.Path.Value)); +#if WINDOWS + fileName += ".exe"; +#endif + } + else + { +#if WINDOWS + fileName = "DedicatedServer.exe"; +#else + fileName = "./DedicatedServer"; +#endif + } string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -899,19 +915,10 @@ namespace Barotrauma } int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); arguments += " -ownerkey " + ownerKey; - - string filename = Path.Combine( - Path.GetDirectoryName(exeName), - Path.GetFileNameWithoutExtension(exeName)); -#if WINDOWS - filename += ".exe"; -#else - filename = "./" + exeName; -#endif - + var processInfo = new ProcessStartInfo { - FileName = filename, + FileName = fileName, Arguments = arguments, WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG @@ -958,15 +965,42 @@ namespace Barotrauma } } + private void UpdateOutOfDateWorkshopItemCount() + { + if (DateTime.Now < modUpdateStatus.WhenToRefresh) { return; } + if (!SteamManager.IsInitialized) { return; } + + var installedPackages = ContentPackageManager.WorkshopPackages; + + var ids = SteamManager.Workshop.GetSubscribedItemIds() + .Select(id => id.Value) + .Union(installedPackages + .Select(pkg => pkg.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value)); + var count = ids + // Deliberately construct Steamworks.Ugc.Item directly + // to not immediately generate a Workshop data request + .Select(id => new Steamworks.Ugc.Item(id)) + .Count(item => + installedPackages.FirstOrDefault(p + => p.UgcId.TryUnwrap(out SteamWorkshopId id) && id.Value == item.Id) + is { } pkg + // Checking that this item is downloading, waiting to be downloaded + // or is newer than the currently installed copy should be good enough, + // and should still not make a Workshop data request + && (item.IsDownloading + || item.IsDownloadPending + || (item.InstallTime.TryGetValue(out var workshopInstallTime) + && pkg.InstallTime.TryUnwrap(out var localInstallTime) + && localInstallTime < workshopInstallTime))); + + modUpdateStatus = (DateTime.Now + ModUpdateInterval, count); + } + public override void Update(double deltaTime) { - modUpdateTimer -= (float)deltaTime; - if (modUpdateTimer <= 0.0f && modUpdateTask is not { IsCompleted: false }) - { - modUpdateTask = BulkDownloader.GetItemsThatNeedUpdating(); - modUpdateTimer = ModUpdateInterval; - } - #if DEBUG hostServerButton.Enabled = true; #else @@ -976,10 +1010,8 @@ namespace Barotrauma } #endif - if (modUpdateTask is { IsCompletedSuccessfully: true }) - { - modUpdatesButton.Visible = modUpdateTask.Result.Count > 0; - } + UpdateOutOfDateWorkshopItemCount(); + modUpdatesButton.Visible = modUpdateStatus.Count > 0; if (modUpdatesButton.Visible) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index c6956035d..59514c304 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -189,7 +189,8 @@ namespace Barotrauma } public IReadOnlyList GetSubList() - => SubList.Content.Children.Select(c => c.UserData as SubmarineInfo).ToArray(); + => (IReadOnlyList)GameMain.Client?.ServerSubmarines + ?? Array.Empty(); public readonly GUIListBox PlayerList; @@ -929,6 +930,8 @@ namespace Barotrauma var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); + //leave some padding for the vote count text + modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; @@ -981,7 +984,7 @@ namespace Barotrauma } else { - GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); } return true; } @@ -3291,16 +3294,15 @@ namespace Barotrauma { //campaign running settingsBlocker.Visible = true; - CampaignFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); - ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.HasPermission(ClientPermissions.ManageRound)); - QuitCampaignButton.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound); + ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && CampaignFrame.Visible; CampaignSetupFrame.Visible = false; } else { CampaignFrame.Visible = false; CampaignSetupFrame.Visible = true; - if (!GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)) + if (!CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound)) { CampaignSetupFrame.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), @@ -3364,7 +3366,7 @@ namespace Barotrauma CampaignFrame.Visible = CampaignSetupFrame.Visible = false; } RefreshEnabledElements(); - if (enabled) + if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign) { ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 0d77adef9..3e95da567 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -543,13 +543,6 @@ namespace Barotrauma } }; - var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), paddedTopPanel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; } - }; - disclaimerBtn.RectTransform.MaxSize = new Point(disclaimerBtn.Rect.Height); - TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y)); paddedTopPanel.Recalculate(); @@ -1425,7 +1418,7 @@ namespace Barotrauma else if (MainSub == null) { var subInfo = new SubmarineInfo(); - MainSub = new Submarine(subInfo); + MainSub = new Submarine(subInfo, showErrorMessages: false); } MainSub.UpdateTransform(interpolate: false); @@ -1462,11 +1455,6 @@ namespace Barotrauma ImageManager.OnEditorSelected(); ReconstructLayers(); - - if (!GameSettings.CurrentConfig.EditorDisclaimerShown) - { - GameMain.Instance.ShowEditorDisclaimer(); - } } public override void OnFileDropped(string filePath, string extension) @@ -2726,11 +2714,13 @@ namespace Barotrauma previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); - var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true); GUIButton createTabberBtn(string labelTag) { var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); + btn.TextBlock.Wrap = true; + btn.TextBlock.SetTextPos(); btn.RectTransform.MaxSize = RectTransform.MaxPoint; btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); btn.Font = GUIStyle.SmallFont; @@ -5635,8 +5625,7 @@ namespace Barotrauma MouseDragStart = Vector2.Zero; } - if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) - && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) + if ((GUI.MouseOn == null || !GUI.MouseOn.IsChildOf(TopPanel)) && dummyCharacter?.SelectedItem == null && !WiringMode && (GUI.MouseOn == null || MapEntity.SelectedAny || MapEntity.SelectionPos != Vector2.Zero)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 594e36ad4..3214a3ff5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -779,6 +779,8 @@ namespace Barotrauma workshopMenu = Screen.Selected is MainMenuScreen ? (WorkshopMenu)new MutableWorkshopMenu(content) : (WorkshopMenu)new ImmutableWorkshopMenu(content); + + GameMain.MainMenuScreen.ResetModUpdateButton(); } private void CreateBottomButtons() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 835981330..575e822eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -648,6 +648,12 @@ namespace Barotrauma.Sounds if (isConnected == 0) { + if (!GameMain.Instance.HasLoaded) + { + //wait for loading to finish so we don't start releasing and reloading sounds when they're being loaded, + //or throw an error mid-loading that'd prevent the content package from being enabled + return; + } DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); SetAudioOutputDevice(""); Disconnected = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index bb5f74966..64ba580ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -865,17 +865,18 @@ namespace Barotrauma public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { + var suitableSounds = damageSounds.Where(s => + s.DamageType == damageType && + (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))); + //if the damage is too low for any sound, don't play anything - if (damageSounds.All(d => damage < d.DamageRange.X)) { return; } + if (suitableSounds.All(d => damage < d.DamageRange.X)) { return; } //allow the damage to differ by 10 from the configured damage range, //so the same amount of damage doesn't always play the same sound float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); - - var suitableSounds = damageSounds.Where(s => - s.DamageType == damageType && - (s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)) && - (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))); + suitableSounds = suitableSounds.Where(s => + s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)); var damageSound = suitableSounds.GetRandomUnsynced(); damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs index 72a4861b5..08c6ace39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs @@ -45,18 +45,26 @@ namespace Barotrauma.Steam protected static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - protected static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container) + protected static void CreateBBCodeElement(Steamworks.Ugc.Item workshopItem, GUIListBox container) { Point cachedContainerSize = Point.Zero; List bbWords = new List(); Stack tagStack = new Stack(); - void recalculate() + string bbCode = ""; + + void forceReset() { - if (cachedContainerSize == container.Content.RectTransform.NonScaledSize) { return; } + bbWords.Clear(); + cachedContainerSize = Point.Zero; + } + + void recalculate(GUICustomComponent component) + { + if (cachedContainerSize == component.RectTransform.NonScaledSize) { return; } bbWords.Clear(); - cachedContainerSize = container.Content.RectTransform.NonScaledSize; + cachedContainerSize = component.RectTransform.NonScaledSize; var matches = new Stack(bbTagRegex.Matches(bbCode).Reverse()); Match? nextTag = null; @@ -133,11 +141,14 @@ namespace Barotrauma.Steam { bbWords.Add(new BBWord(finalWord, currTagType)); } + + container.RecalculateChildren(); + container.UpdateScrollBarSize(); } void draw(SpriteBatch spriteBatch, GUICustomComponent component) { - recalculate(); + recalculate(component); Vector2 currPos = Vector2.Zero; Vector2 rectPos = component.Rect.Location.ToVector2(); for (int i = 0; i < bbWords.Count; i++) @@ -180,7 +191,19 @@ namespace Barotrauma.Steam = component.RectTransform.NonScaledSize.ToVector2() / component.Parent.Rect.Size.ToVector2(); } - return new GUICustomComponent(new RectTransform(Vector2.One, container.Content.RectTransform), + TaskPool.Add( + $"GetWorkshopItemLongDescriptionFor{workshopItem.Id.Value}", + SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? workshopItemWithDescription)) { return; } + + bbCode = workshopItemWithDescription?.Description ?? ""; + forceReset(); + }); + + new GUICustomComponent( + new RectTransform(Vector2.One, container.Content.RectTransform), onDraw: draw); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 4331f34bb..b8688c2f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -34,7 +34,7 @@ namespace Barotrauma.Steam if (numSubscribedMods == memSubscribedModCount) { return; } memSubscribedModCount = numSubscribedMods; - var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); + var subscribedIds = SteamManager.Workshop.GetSubscribedItemIds(); var installedIds = ContentPackageManager.WorkshopPackages .Select(p => p.UgcId) .NotNone() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 62e930476..415bda37b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -773,7 +773,7 @@ namespace Barotrauma.Steam #endregion var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); - CreateBBCodeElement(workshopItem.Description, descriptionListBox); + CreateBBCodeElement(workshopItem, descriptionListBox); var showInSteamContainer = new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index c3d167fed..e3de26b71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -218,6 +218,20 @@ namespace Barotrauma.Steam var descriptionTextBox = ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty); + if (workshopItem.Id != 0) + { + TaskPool.Add( + $"GetFullDescription{workshopItem.Id}", + SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? itemWithDescription)) { return; } + + descriptionTextBox.Text = itemWithDescription?.Description ?? descriptionTextBox.Text; + descriptionTextBox.Deselect(); + }); + } + var (leftBottom, _, rightBottom) = CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f); leftBottom.Stretch = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs index 7443204bb..784421558 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs @@ -2,7 +2,7 @@ namespace Barotrauma { - partial class UpgradePrefab + sealed partial class UpgradePrefab { public readonly ImmutableArray DecorativeSprites = new ImmutableArray(); public Sprite Sprite { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index f1fc1456d..45111625f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -23,36 +23,55 @@ namespace Barotrauma public static void ConvertMasterLocalizationKit(string outputTextsDirectory, string outputConversationsDirectory, bool convertConversations) { - string textFilePath = Path.Combine(infoTextPath, "Texts.csv"); - string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv"); + List languages = new List(); + for (int i = 0; i < 2; i++) + { + string textFilePath; + string outputFileName; + switch (i) + { + case 0: + textFilePath = Path.Combine(infoTextPath, "Texts.csv"); + outputFileName = "Vanilla.xml"; + break; + case 1: + textFilePath = Path.Combine(infoTextPath, "EditorTexts.csv"); + outputFileName = "VanillaEditorTexts.xml"; + break; + default: + throw new NotImplementedException(); + } - Dictionary> xmlContent; - try - { - xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); - } - catch (Exception e) - { - DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e); - return; - } - if (xmlContent == null) - { - DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath); - return; - } - foreach (string language in xmlContent.Keys) - { - string languageNoWhitespace = language.Replace(" ", ""); - string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}Vanilla.xml"); - File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8); - DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); + Dictionary> xmlContent; + try + { + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); + } + catch (Exception e) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e); + return; + } + if (xmlContent == null) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath); + return; + } + foreach (string language in xmlContent.Keys) + { + languages.Add(language); + string languageNoWhitespace = language.Replace(" ", ""); + string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}{outputFileName}"); + File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8); + DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); + } } if (convertConversations) { + string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv"); var conversationLinesAll = File.ReadAllLines(conversationFilePath, Encoding.UTF8); - foreach (string language in xmlContent.Keys) + foreach (string language in languages) { List convXmlContent = ConvertConversationsToXML(conversationLinesAll, language); if (convXmlContent == null) @@ -61,7 +80,7 @@ namespace Barotrauma continue; } string languageNoWhitespace = language.Replace(" ", ""); - string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"NpcConversations_{languageNoWhitespace}.xml"); + string xmlFileFullPath = Path.Combine(outputConversationsDirectory, languageNoWhitespace, $"NpcConversations_{languageNoWhitespace}.xml"); File.WriteAllLines(xmlFileFullPath, convXmlContent, Encoding.UTF8); DebugConsole.NewMessage("Conversation localization .xml file successfully created at: " + xmlFileFullPath); } @@ -339,7 +358,8 @@ namespace Barotrauma string[] headerSplit = csvContent[0].Split(separator); for (int i = 0; i < headerSplit.Length; i++) { - if (headerSplit[i] == language) + if (headerSplit[i] == language || + (language == "English" && headerSplit[i]== "Line (Original)")) { languageColumn = i; break; @@ -348,6 +368,7 @@ namespace Barotrauma xmlContent.Add($""); + conversationClosingIndent.Clear(); int conversationStart = 1; xmlContent.Add(string.Empty); @@ -399,9 +420,9 @@ namespace Barotrauma { string[] nextConversationElement = csvContent[i + 1].Split(separator); - if (nextConversationElement[1] != string.Empty) + if (nextConversationElement[3] != string.Empty) { - nextDepth = int.Parse(nextConversationElement[2]); + nextDepth = int.Parse(nextConversationElement[3]); nextIsSubConvo = nextDepth > depthIndex; } @@ -421,7 +442,12 @@ namespace Barotrauma } else { + //end of file, close remaining xml tags xmlContent.Add(element.TrimEnd() + "/>"); + for (int j = depthIndex - 1; j >= 0; j--) + { + HandleClosingElements(xmlContent, j); + } } } @@ -433,12 +459,12 @@ namespace Barotrauma private static void HandleClosingElements(List xmlContent, int targetDepth) { - if (conversationClosingIndent.Count == 0) return; + if (conversationClosingIndent.Count == 0) { return; } for (int k = conversationClosingIndent.Count - 1; k >= 0; k--) { int currentIndent = conversationClosingIndent[k]; - if (currentIndent < targetDepth) break; + if (currentIndent < targetDepth) { break; } xmlContent.Add($"{GetIndenting(currentIndent)}"); conversationClosingIndent.RemoveAt(k); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index 43ba00158..f0315b696 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -7,28 +7,30 @@ using System.Text; namespace Barotrauma { - class SpriteRecorder : ISpriteBatch, IDisposable + sealed class SpriteRecorder : ISpriteBatch, IDisposable { - private struct Command + private readonly record struct Command( + Texture2D Texture, + VertexPositionColorTexture VertexBL, + VertexPositionColorTexture VertexBR, + VertexPositionColorTexture VertexTL, + VertexPositionColorTexture VertexTR, + float Depth, + Vector2 Min, + Vector2 Max, + int Index) { - public readonly Texture2D Texture; - public readonly VertexPositionColorTexture VertexBL; - public readonly VertexPositionColorTexture VertexBR; - public readonly VertexPositionColorTexture VertexTL; - public readonly VertexPositionColorTexture VertexTR; - public readonly float Depth; - public readonly Vector2 Min; - public readonly Vector2 Max; - public readonly int Index; - - public bool Overlaps(Command other) - { - return - Min.X <= other.Max.X && Max.X >= other.Min.X && - Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; - } - - public Command( + public static Vector2 GetMinPosition(params VertexPositionColorTexture[] vertices) + => new Vector2( + MathUtils.Min(vertices.Select(v => v.Position.X).ToArray()), + MathUtils.Min(vertices.Select(v => v.Position.Y).ToArray())); + + public static Vector2 GetMaxPosition(params VertexPositionColorTexture[] vertices) + => new Vector2( + MathUtils.Max(vertices.Select(v => v.Position.X).ToArray()), + MathUtils.Max(vertices.Select(v => v.Position.Y).ToArray())); + + public static Command FromTransform( Texture2D texture, Vector2 pos, Rectangle srcRect, @@ -46,15 +48,11 @@ namespace Barotrauma int srcRectBottom = srcRect.Bottom; if (effects.HasFlag(SpriteEffects.FlipHorizontally)) { - var temp = srcRectRight; - srcRectRight = srcRectLeft; - srcRectLeft = temp; + (srcRectRight, srcRectLeft) = (srcRectLeft, srcRectRight); } if (effects.HasFlag(SpriteEffects.FlipVertically)) { - var temp = srcRectBottom; - srcRectBottom = srcRectTop; - srcRectTop = temp; + (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); } rotation = MathHelper.ToRadians(rotation); @@ -68,59 +66,63 @@ namespace Barotrauma pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; - Texture = texture; + var vertexTl = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X, pos.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height) + }; - Depth = depth; + var vertexTr = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height) + }; - VertexTL.Color = color; - VertexTR.Color = color; - VertexBL.Color = color; - VertexBR.Color = color; + var vertexBl = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) + }; - VertexTL.Position = new Vector3(pos.X, pos.Y, 0f); - VertexTR.Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f); - VertexBL.Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f); - VertexBR.Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f); + var vertexBr = new VertexPositionColorTexture + { + Color = color, + Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f), + TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height) + }; - Min = new Vector2( - MathUtils.Min - ( - VertexTL.Position.X, - VertexTR.Position.X, - VertexBL.Position.X, - VertexBR.Position.X - ), - MathUtils.Min - ( - VertexTL.Position.Y, - VertexTR.Position.Y, - VertexBL.Position.Y, - VertexBR.Position.Y - )); + var min = GetMinPosition( + vertexTl, + vertexTr, + vertexBl, + vertexBr); - Max = new Vector2( - MathUtils.Max - ( - VertexTL.Position.X, - VertexTR.Position.X, - VertexBL.Position.X, - VertexBR.Position.X - ), - MathUtils.Max - ( - VertexTL.Position.Y, - VertexTR.Position.Y, - VertexBL.Position.Y, - VertexBR.Position.Y - )); + var max = GetMaxPosition( + vertexTl, + vertexTr, + vertexBl, + vertexBr); - VertexTL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height); - VertexTR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height); - VertexBL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); - VertexBR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); - - Index = index; + return new Command( + texture, + vertexBl, + vertexBr, + vertexTl, + vertexTr, + depth, + min, + max, + index); + } + public bool Overlaps(Command other) + { + return + Min.X <= other.Max.X && Max.X >= other.Min.X && + Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; } } @@ -151,8 +153,8 @@ namespace Barotrauma public static BasicEffect BasicEffect = null; - private List recordedBuffers = new List(); - private List commandList = new List(); + private readonly List recordedBuffers = new List(); + private readonly List commandList = new List(); private SpriteSortMode currentSortMode; private IndexBuffer indexBuffer = null; @@ -170,16 +172,45 @@ namespace Barotrauma currentSortMode = sortMode; } - public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + private void AppendCommand(Command command) { if (isDisposed) { return; } - - Command command = new Command(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList?.Count ?? 0); + if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); - commandList?.Add(command); + commandList.Add(command); + } + + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, 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); + AppendCommand(command); + } + + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null) + { + if (isDisposed) { return; } + + int iters = count ?? (vertices.Length / 4); + for (int i=0;iBarotrauma FakeFish, Undertow Games Barotrauma - 0.20.15.0 + 0.21.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 5ec2fca2c..36467864b 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.15.0 + 0.21.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 7e1afaeca..671b920b4 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.15.0 + 0.21.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 81904c7cd..422aaffe1 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.15.0 + 0.21.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 474f98777..be8a82f6f 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.15.0 + 0.21.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index eedb1f10b..b960f2f7d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -8,8 +8,9 @@ namespace Barotrauma { partial class Character { - public Address OwnerClientAddress; - public string OwnerClientName; + private Address ownerClientAddress; + private Option ownerClientAccountId; + public bool ClientDisconnected; public float KillDisconnectedTimer; @@ -19,6 +20,35 @@ namespace Barotrauma public bool HealthUpdatePending; + public void SetOwnerClient(Client client) + { + if (client == null) + { + ownerClientAddress = null; + ownerClientAccountId = Option.None(); + IsRemotePlayer = false; + } + else + { + ownerClientAddress = client.Connection.Endpoint.Address; + ownerClientAccountId = client.AccountId; + IsRemotePlayer = true; + } + } + + public bool IsClientOwner(Client client) + { + if (ownerClientAccountId.TryUnwrap(out var accountId) + && client.AccountId.TryUnwrap(out var clientId)) + { + return accountId == clientId; + } + else + { + return ownerClientAddress == client.Connection.Endpoint.Address; + } + } + public float GetPositionUpdateInterval(Client recipient) { if (!Enabled) { return 1000.0f; } @@ -302,12 +332,8 @@ namespace Barotrauma } } - public void ServerWritePosition(IWriteMessage msg, Client c) + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { - msg.WriteUInt16(ID); - - IWriteMessage tempBuffer = new WriteOnlyMessage(); - if (this == c.Character) { tempBuffer.WriteBoolean(true); @@ -405,11 +431,6 @@ namespace Barotrauma AIController?.ServerWrite(tempBuffer); HealthUpdatePending = false; } - - tempBuffer.WritePadBits(); - - msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); } public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 9749fec03..9379178b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1374,7 +1374,7 @@ namespace Barotrauma MultiPlayerCampaign.StartCampaignSetup(); return; } - if (!GameMain.Server.StartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } + if (!GameMain.Server.TryStartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } } })); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 40f8f9f24..ea3362d68 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -66,6 +66,7 @@ namespace Barotrauma public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; + public readonly string[] CommandLineArgs; public GameMain(string[] args) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 4283c7206..f3d53216a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -27,12 +27,9 @@ namespace Barotrauma AnyOneAllowedToManageCampaign(permissions); } - public bool AllowedToManageWallets(Client client) + public static bool AllowedToManageWallets(Client client) { - return - client.HasPermission(ClientPermissions.ManageCampaign) || - client.HasPermission(ClientPermissions.ManageMoney) || - IsOwner(client); + return AllowedToManageCampaign(client, ClientPermissions.ManageMoney); } public override void ShowStartMessage() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6d879caf7..089758334 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -146,7 +146,7 @@ namespace Barotrauma { NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData; MirrorLevel = false; - GameMain.Server.StartGame(); + GameMain.Server.TryStartGame(); } public static void StartCampaignSetup() @@ -395,7 +395,7 @@ namespace Barotrauma yield return new WaitForSeconds(EndTransitionDuration * 0.5f); } - GameMain.Server.StartGame(); + GameMain.Server.TryStartGame(); yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 60cc5c609..de8517914 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components msg.WriteBoolean(launch); if (launch) { - msg.WriteUInt16(User.ID); + msg.WriteUInt16(User?.ID ?? Entity.NullEntityID); msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 3dc30f6cf..e04c02d33 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -348,15 +348,9 @@ namespace Barotrauma } } - public void ServerWritePosition(IWriteMessage msg, Client c) + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { - msg.WriteUInt16(ID); - - IWriteMessage tempBuffer = new WriteOnlyMessage(); body.ServerWrite(tempBuffer); - msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); - msg.WritePadBits(); } public void CreateServerEvent(T ic) where T : ItemComponent, IServerSerializable @@ -378,7 +372,7 @@ namespace Barotrauma if (!components.Contains(ic)) { return; } var eventData = new ComponentStateEventData(ic, extraData); - if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } + if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false."); } GameMain.Server.CreateEntityEvent(this, eventData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 572ffaab8..2f2a1e174 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -5,14 +5,9 @@ namespace Barotrauma { partial class Submarine { - public void ServerWritePosition(IWriteMessage msg, Client c) + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { - msg.WriteUInt16(ID); - IWriteMessage tempBuffer = new WriteOnlyMessage(); subBody.Body.ServerWrite(tempBuffer); - msg.WriteByte((byte)tempBuffer.LengthBytes); - msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); - msg.WritePadBits(); } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 7f33ad6b8..a12693b51 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -58,6 +58,7 @@ namespace Barotrauma.Networking private DateTime roundStartTime; + private bool wasReadyToStartAutomatically; private bool autoRestartTimerRunning; private float endRoundTimer; @@ -366,8 +367,7 @@ namespace Barotrauma.Networking character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); - Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.AddressMatches(character.OwnerClientAddress)); - + Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) { character.Kill(CauseOfDeathType.Disconnected, null); @@ -504,8 +504,7 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame && - (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) + else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame) { if (ServerSettings.AutoRestart) { @@ -526,18 +525,25 @@ namespace Barotrauma.Networking } } + bool readyToStartAutomatically = false; if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f) { - StartGame(); + readyToStartAutomatically = true; } else if (ServerSettings.StartWhenClientsReady) { int clientsReady = connectedClients.Count(c => c.GetVote(VoteType.StartRound)); if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio) { - StartGame(); + readyToStartAutomatically = true; } } + if (readyToStartAutomatically) + { + if (!wasReadyToStartAutomatically) { GameMain.NetLobbyScreen.LastUpdateID++; } + TryStartGame(); + } + wasReadyToStartAutomatically = readyToStartAutomatically; } for (int i = disconnectedClients.Count - 1; i >= 0; i--) @@ -763,7 +769,7 @@ namespace Barotrauma.Networking else { string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); - if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) + if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { ServerSettings.CampaignSettings = settings; ServerSettings.SaveSettings(); @@ -779,7 +785,10 @@ namespace Barotrauma.Networking SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } - if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } + if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) + { + MultiPlayerCampaign.LoadCampaign(saveName); + } } break; case ClientPacketHeader.VOICE: @@ -1389,13 +1398,13 @@ namespace Barotrauma.Networking if (end) { if (mpCampaign == null || - CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) || - CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) + CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound)) { bool save = inc.ReadBoolean(); + bool quitCampaign = inc.ReadBoolean(); if (GameStarted) { - Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); @@ -1409,6 +1418,14 @@ namespace Barotrauma.Networking } EndGame(wasSaved: save); } + else if (mpCampaign != null) + { + Log($"Client \"{ClientLogName(sender)}\" quit the currently active campaign.", ServerLog.MessageType.ServerMessage); + GameMain.GameSession = null; + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.Sandbox.Identifier; + GameMain.NetLobbyScreen.LastUpdateID++; + + } } } else @@ -1425,12 +1442,11 @@ namespace Barotrauma.Networking { MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); } - } else if (!GameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - StartGame(); + TryStartGame(); } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { @@ -1492,20 +1508,10 @@ namespace Barotrauma.Networking case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; - Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name.Value, ServerLog.MessageType.ServerMessage); - - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign") + Log("Gamemode changed to " + (GameMain.NetLobbyScreen.SelectedMode?.Name.Value ?? "none"), ServerLog.MessageType.ServerMessage); + if (GameMain.NetLobbyScreen.GameModes[modeIndex] == GameModePreset.MultiPlayerCampaign) { - const int MaxSaves = 255; - var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); - IWriteMessage msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); - for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) - { - msg.WriteNetSerializableStruct(saveInfos[i]); - } - serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); + TrySendCampaignSetupInfo(sender); } break; case ClientPermissions.ManageCampaign: @@ -1612,7 +1618,7 @@ namespace Barotrauma.Networking { if (GameSettings.CurrentConfig.VerboseLogging) { - DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); + DebugConsole.NewMessage($"Sending initial lobby update to {c.Name}", Color.Gray); } outmsg.WriteByte(c.SessionId); @@ -1745,9 +1751,9 @@ namespace Barotrauma.Networking continue; } - IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); - tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); + var tempBuffer = new ReadWriteMessage(); + var entityPositionHeader = EntityPositionHeader.FromEntity(entity); + tempBuffer.WriteNetSerializableStruct(entityPositionHeader); entityPositionSync.ServerWritePosition(tempBuffer, c); //no more room in this packet @@ -1758,6 +1764,7 @@ namespace Barotrauma.Networking segmentTable.StartNewSegment(ServerNetSegment.EntityPosition); outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + outmsg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); @@ -1934,6 +1941,13 @@ namespace Barotrauma.Networking { outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); } + + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign && + connectedClients.None(c => c.Connection == OwnerConnection || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign))) + { + //if no-one has permissions to manage the campaign, show the setup UI to everyone + TrySendCampaignSetupInfo(c); + } } else { @@ -1943,9 +1957,8 @@ namespace Barotrauma.Networking settingsBytes = outmsg.LengthBytes - settingsBytes; int campaignBytes = outmsg.LengthBytes; - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.WriteBoolean(true); outmsg.WritePadBits(); @@ -2023,7 +2036,7 @@ namespace Barotrauma.Networking } } - private void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) + private static void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2037,10 +2050,26 @@ namespace Barotrauma.Networking } } - public bool StartGame() + public bool TryStartGame() { if (initiatedStartGame || GameStarted) { return false; } + GameModePreset selectedMode = + Voting.HighestVoted(VoteType.Mode, connectedClients) ?? GameMain.NetLobbyScreen.SelectedMode; + if (selectedMode == null) + { + return false; + } + if (selectedMode == GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is not MultiPlayerCampaign) + { + //DebugConsole.ThrowError($"{nameof(TryStartGame)} failed. Cannot start a multiplayer campaign via {nameof(TryStartGame)} - use {nameof(MultiPlayerCampaign.StartNewCampaign)} or {nameof(MultiPlayerCampaign.LoadCampaign)} instead."); + if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) + { + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.MultiPlayerCampaign.Identifier; + } + return false; + } + Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; @@ -2060,23 +2089,13 @@ namespace Barotrauma.Networking return false; } - GameModePreset selectedMode = Voting.HighestVoted(VoteType.Mode, connectedClients); - if (selectedMode == null) { selectedMode = GameMain.NetLobbyScreen.SelectedMode; } - if (selectedMode == null) - { - return false; - } - if (selectedMode == GameModePreset.MultiPlayerCampaign && !(GameMain.GameSession?.GameMode is CampaignMode)) - { - DebugConsole.ThrowError("StartGame failed. Cannot start a multiplayer campaign via StartGame - use MultiPlayerCampaign.StartNewCampaign or MultiPlayerCampaign.LoadCampaign instead."); - return false; - } initiatedStartGame = true; - startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); + startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); return true; } + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) { initiatedStartGame = true; @@ -2168,7 +2187,6 @@ namespace Barotrauma.Networking initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true }; } - List playingClients = new List(connectedClients); if (ServerSettings.AllowSpectating) { @@ -2413,8 +2431,7 @@ namespace Barotrauma.Networking mpCampaign.ClearSavedExperiencePoints(teamClients[i]); } - spawnedCharacter.OwnerClientAddress = teamClients[i].Connection.Endpoint.Address; - spawnedCharacter.OwnerClientName = teamClients[i].Name; + spawnedCharacter.SetOwnerClient(teamClients[i]); } for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) @@ -2479,7 +2496,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; - Voting?.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); + Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); GameMain.GameScreen.Select(); @@ -2507,14 +2524,11 @@ namespace Barotrauma.Networking private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize) { - MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.STARTGAME); msg.WriteInt32(seed); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); - bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); @@ -2530,7 +2544,7 @@ namespace Barotrauma.Networking ServerSettings.WriteMonsterEnabled(msg); - if (campaign == null) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { msg.WriteString(levelSeed); msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); @@ -2566,6 +2580,23 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } + private bool TrySendCampaignSetupInfo(Client client) + { + if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } + + const int MaxSaves = 255; + var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); + for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + { + msg.WriteNetSerializableStruct(saveInfos[i]); + } + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + return true; + } + private bool IsUsingRespawnShuttle() { return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle); @@ -3521,9 +3552,7 @@ namespace Barotrauma.Networking //the client's previous character is no longer a remote player if (client.Character != null) { - client.Character.IsRemotePlayer = false; - client.Character.OwnerClientAddress = null; - client.Character.OwnerClientName = null; + client.Character.SetOwnerClient(null); } if (newCharacter == null) @@ -3549,9 +3578,7 @@ namespace Barotrauma.Networking newCharacter.Info.Character = newCharacter; } - newCharacter.OwnerClientAddress = client.Connection.Endpoint.Address; - newCharacter.OwnerClientName = client.Name; - newCharacter.IsRemotePlayer = true; + newCharacter.SetOwnerClient(client); newCharacter.Enabled = true; client.Character = newCharacter; CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 4f93198d9..17d19a55f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -439,8 +439,7 @@ namespace Barotrauma.Networking } clients[i].Character = character; - character.OwnerClientAddress = clients[i].Connection.Endpoint.Address; - character.OwnerClientName = clients[i].Name; + character.SetOwnerClient(clients[i]); GameServer.Log( $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index be597c2da..3eaee94aa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -117,13 +117,13 @@ namespace Barotrauma public void StopSubmarineVote(bool passed) { - if (!(ActiveVote is SubmarineVote)) { return; } + if (ActiveVote is not SubmarineVote) { return; } StopActiveVote(passed); } public void StopMoneyTransferVote(bool passed) { - if (!(ActiveVote is TransferVote)) { return; } + if (ActiveVote is not TransferVote) { return; } StopActiveVote(passed); } @@ -155,7 +155,7 @@ namespace Barotrauma GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } - private void StartOrEnqueueVote(IVote vote) + private static void StartOrEnqueueVote(IVote vote) { if (ActiveVote == null) { @@ -198,9 +198,9 @@ namespace Barotrauma ActiveVote.Timer += deltaTime; - if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout || inGameClients.Count() == 1) { - var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter); // Do not take unanswered into account for total @@ -216,7 +216,7 @@ namespace Barotrauma } } - public void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) + public static void ResetVotes(IEnumerable connectedClients, bool resetKickVotes) { foreach (Client client in connectedClients) { @@ -254,7 +254,14 @@ namespace Barotrauma string modeIdentifier = inc.ReadString(); GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); if (mode == null || !mode.Votable) { break; } + var prevHighestVoted = HighestVoted(VoteType.Mode, GameMain.Server.ConnectedClients); sender.SetVote(voteType, mode); + var newHighestVoted = HighestVoted(VoteType.Mode, GameMain.Server.ConnectedClients); + if (prevHighestVoted != newHighestVoted) + { + GameMain.NetLobbyScreen.SelectedModeIdentifier = mode.Identifier; + GameMain.NetLobbyScreen.LastUpdateID++; + } break; case VoteType.EndRound: if (!sender.HasSpawned) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index ec2ca5d40..eb1a00ee9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -59,6 +59,7 @@ namespace Barotrauma get { return GameModes[SelectedModeIndex].Identifier; } set { + if (SelectedModeIdentifier == value) { return; } for (int i = 0; i < GameModes.Length; i++) { if (GameModes[i].Identifier == value) @@ -127,9 +128,11 @@ namespace Barotrauma { LevelSeed = ToolBox.RandomSeed(8); - subs = SubmarineInfo.SavedSubmarines.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)).ToList(); + subs = SubmarineInfo.SavedSubmarines + .Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)) + .ToList(); - if (subs == null || subs.Count() == 0) + if (subs == null || subs.Count == 0) { throw new Exception("No submarines are available."); } @@ -156,7 +159,7 @@ namespace Barotrauma GameModes = GameModePreset.List.ToArray(); } - private List subs; + private readonly List subs; public IReadOnlyList GetSubList() => subs; public string LevelSeed @@ -192,7 +195,7 @@ namespace Barotrauma public override void Select() { base.Select(); - GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); + Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 8539178dd..25a9ba4aa 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.15.0 + 0.21.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index 40605ddf4..ef782ae23 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -11,6 +11,7 @@ RadiationEnabled="false" StartingBalanceAmount="High" StartItemSet="easy" + MaxMissionCount="3" Difficulty="Easy"/> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index f0eae6be8..7820a24dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -351,6 +351,21 @@ namespace Barotrauma } } + public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) + { + if (myTeam == otherTeam) { return true; } + return myTeam switch + { + // NPCs are friendly to the same team and the friendly NPCs + CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, + // Friendly NPCs are friendly to both player teams + CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, + _ => true + }; + } + + public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); + public void ReequipUnequipped() { foreach (var item in unequippedItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 6d0e43c18..378052dc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -355,6 +355,10 @@ namespace Barotrauma { targetingTag = "owner"; } + else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter)) + { + targetingTag = "hostile"; + } else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) { targetingTag = tP.Tag; @@ -365,7 +369,7 @@ namespace Barotrauma { targetingTag = "husk"; } - else if (!Character.IsFriendly(targetCharacter)) + else if (!Character.IsSameSpeciesOrGroup(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { @@ -687,12 +691,9 @@ namespace Barotrauma return a.Damage >= selectedTargetingParams.Threshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; - //if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState - //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it - //TODO: a way for pets to differentiate hostile and friendly humans? - if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter)) + if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker)) { - // Attack the character that attacked the target we are protecting + // Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group. ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); State = AIState.Attack; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 32ce66986..3093150d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1514,9 +1514,18 @@ namespace Barotrauma startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f); //do a raycast upwards to find any walls - float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f; + if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider)) + { + mainCollider = Character.AnimController.Collider; + } + float margin = 0.1f; + if (shouldCrouch) + { + margin *= 2; + } + float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin; - shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null; + shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null; } public bool AllowCampaignInteraction() @@ -1589,7 +1598,27 @@ namespace Barotrauma (!requireEquipped || character.HasEquippedItem(i)) && (predicate == null || predicate(i)), recursive, matchingItems); items = matchingItems; - return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + foreach (var item in matchingItems) + { + if (item == null) { continue; } + + if (containedTag.IsEmpty || item.OwnInventory == null) + { + //no contained items required, this item's ok + return true; + } + var suitableSlot = item.GetComponent().FindSuitableSubContainerIndex(containedTag); + if (suitableSlot == null) + { + //no restrictions on the suitable slot + return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage); + } + else + { + return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value)); + } + } + return false; } public static void StructureDamaged(Structure structure, float damageAmount, Character character) @@ -2016,11 +2045,9 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - bool friendlyTeam = IsOnFriendlyTeam(me, other); - bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; + bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other); if (!teamGood) { return false; } - bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); - if (!speciesGood) { return false; } + if (!me.IsSameSpeciesOrGroup(other)) { return false; } if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { var reputation = campaign.Map?.CurrentLocation?.Reputation; @@ -2029,30 +2056,14 @@ namespace Barotrauma return false; } } + if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet) + { + // Hostile NPCs are hostile to all pets, unless they are in the same team. + return false; + } return true; } - public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) - { - if (myTeam == otherTeam) { return true; } - - switch (myTeam) - { - case CharacterTeamType.None: - case CharacterTeamType.Team1: - case CharacterTeamType.Team2: - // Only friendly to the same team and friendly NPCs - return otherTeam == CharacterTeamType.FriendlyNPC; - case CharacterTeamType.FriendlyNPC: - // Friendly NPCs are friendly to both teams - return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2; - default: - return true; - } - } - - public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); - public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsTrueForAllCrewMembers(Character character, Func predicate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index a9ae4cbf7..765b8de83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -98,7 +98,7 @@ namespace Barotrauma int containedItemCount = 0; foreach (Item it in container.Inventory.AllItems) { - if (CheckItem(it)) + if (CheckItem(it) && IsInTargetSlot(it)) { containedItemCount++; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 0c3ae42a1..a87e5cd61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,5 +1,5 @@ -using Barotrauma.Items.Components; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -18,6 +18,7 @@ namespace Barotrauma private AIObjectiveGetItem getDivingGear; private AIObjectiveContainItem getOxygen; private Item targetItem; + private int? oxygenSourceSlotIndex; public const float MIN_OXYGEN = 10; @@ -43,12 +44,15 @@ namespace Barotrauma Abandon = true; return; } - targetItem = character.Inventory.FindItemByTag(gearTag, true); + + TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true)); if (targetItem == null && gearTag == LIGHT_DIVING_GEAR) { - targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true); + TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true)); } - if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) + if (targetItem == null || + !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && + targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) { TryAddSubObjective(ref getDivingGear, () => { @@ -84,7 +88,7 @@ namespace Barotrauma else { float min = GetMinOxygen(character); - if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min)) + if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it))) { TryAddSubObjective(ref getOxygen, () => { @@ -93,7 +97,7 @@ namespace Barotrauma if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) { character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); - if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) + if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1) { character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); } @@ -109,7 +113,8 @@ namespace Barotrauma AllowToFindDivingGear = false, AllowDangerousPressure = true, ConditionLevel = MIN_OXYGEN, - RemoveExistingWhenNecessary = true + RemoveExistingWhenNecessary = true, + TargetSlot = oxygenSourceSlotIndex }; if (container.HasSubContainers) { @@ -167,12 +172,36 @@ namespace Barotrauma } } + private bool IsSuitableContainedOxygenSource(Item item) + { + return + item != null && + item.HasTag(OXYGEN_SOURCE) && + item.Condition > 0 && + (oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value)); + } + + private void TrySetTargetItem(Item item) + { + if (targetItem == item) { return; } + targetItem = item; + if (targetItem != null) + { + oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + } + else + { + oxygenSourceSlotIndex = null; + } + } + public override void Reset() { base.Reset(); getDivingGear = null; getOxygen = null; targetItem = null; + oxygenSourceSlotIndex = null; } public static float GetMinOxygen(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 2eb5b453e..26f10fd92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -71,7 +71,7 @@ namespace Barotrauma Priority = 100; } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && - character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) + character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) { // Ordered to follow, hold position, or return back to main sub inside a hostile sub // -> ignore find safety unless we need to find a diving gear diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index cf8cf19ae..3df33e367 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -413,7 +413,7 @@ namespace Barotrauma } } } - + private void ApplyTreatment(Affliction affliction, Item item) { item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index ceeeab73b..b50108c6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -371,7 +371,8 @@ namespace Barotrauma private int CalculateCellCount(int minValue, int maxValue) { if (maxValue == 0) { return 0; } - float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier); + float difficulty = Level.Loaded?.Difficulty ?? 0.0f; + float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier); return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t)); } @@ -381,7 +382,8 @@ namespace Barotrauma float delay = Config.AgentSpawnDelay; float min = delay; float max = delay * 6; - float t = Level.Loaded.Difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor); + float difficulty = Level.Loaded?.Difficulty ?? 0.0f; + float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor); return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index a2e4e7a3a..140aa7c04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -24,7 +24,7 @@ namespace Barotrauma public bool IsAiming => wasAiming; public bool IsAimingMelee => wasAimingMelee; - protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim); + protected bool Aiming => aiming || aimingMelee || FlipLockTime > Timing.TotalTime && character.IsKeyDown(InputType.Aim); public float ArmLength => upperArmLength + forearmLength; @@ -278,7 +278,11 @@ namespace Barotrauma // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; - public float LockFlippingUntil; + public float FlipLockTime { get; private set; } + public void LockFlipping(float time = 0.2f) + { + FlipLockTime = (float)Timing.TotalTime + time; + } public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index f0818dd66..4e3da61fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -994,7 +994,7 @@ namespace Barotrauma foreach (Limb l in Limbs) { if (l.IsSevered) { continue; } - if (!l.DoesFlip) { continue; } + if (!l.DoesFlip) { continue; } if (RagdollParams.IsSpritesheetOrientationHorizontal) { //horizontally aligned limbs need to be flipped 180 degrees @@ -1014,7 +1014,7 @@ namespace Barotrauma if (l.IsSevered) { continue; } float rotation = l.body.Rotation; - if (l.DoesFlip) + if (l.DoesMirror) { if (RagdollParams.IsSpritesheetOrientationHorizontal) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index b617fef5f..0b1d4cafd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -150,8 +150,10 @@ namespace Barotrauma private readonly float movementLerp; - private float cprAnimTimer; - private float cprPump; + private float cprAnimTimer,cprPump; + + private float fallingProneAnimTimer; + const float FallingProneAnimDuration = 1.0f; private bool swimming; //time until the character can switch from walking to swimming or vice versa @@ -268,7 +270,8 @@ namespace Barotrauma if (deathAnimTimer < deathAnimDuration) { deathAnimTimer += deltaTime; - UpdateDying(deltaTime); + //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration + UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration); } } else @@ -278,6 +281,11 @@ namespace Barotrauma if (!character.CanMove) { + if (fallingProneAnimTimer < FallingProneAnimDuration) + { + fallingProneAnimTimer += deltaTime; + UpdateFallingProne(1.0f); + } levitatingCollider = false; Collider.FarseerBody.FixedRotation = false; if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) @@ -291,18 +299,20 @@ namespace Barotrauma } return; } + fallingProneAnimTimer = 0.0f; //re-enable collider if (!Collider.Enabled) { var lowestLimb = FindLowestLimb(); - + Collider.SetTransform(new Vector2( Collider.SimPosition.X, Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), Collider.Rotation); Collider.FarseerBody.ResetDynamics(); + Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity; Collider.Enabled = true; } @@ -431,7 +441,7 @@ namespace Barotrauma } } - if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) + if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck) { Flip(); } @@ -1292,10 +1302,9 @@ namespace Barotrauma } } - void UpdateDying(float deltaTime) + void UpdateFallingProne(float strength) { - //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration - float strength = 1.0f - deathAnimTimer / deathAnimDuration; + if (strength <= 0.0f) { return; } Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); @@ -1319,6 +1328,19 @@ namespace Barotrauma } if (torso == null) { return; } + + //make the torso tip over + //otherwise it tends to just drop straight down, pinning the characters legs in a weird pose + if (!InWater) + { + //prefer tipping over in the same direction the torso is rotating + //or moving + //or lastly, in the direction it's facing if it's not moving/rotating + float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f); + float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength; + torso.body.ApplyTorque(torque * torso.body.Mass); + } + //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split for (int i = 0; i < 2; i++) { @@ -1503,12 +1525,12 @@ namespace Barotrauma Limb rightHand = GetLimb(LimbType.RightHand); Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); - if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); - if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb; + if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); } + if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; } Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); - if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso); - if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb; + if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); } + if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; } if (!target.AllowInput) { @@ -1644,18 +1666,24 @@ namespace Barotrauma pullLimb.PullJointEnabled = true; if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { - Vector2 pullLimbAnchor = targetLimb.SimPosition; pullLimb.PullJointMaxForce = 5000.0f; if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) { targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); } - - Vector2 shoulderPos = rightShoulder.WorldAnchorA; - Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; - if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; } - targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength); + Vector2 shoulderPos = rightShoulder.WorldAnchorA; + float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos); + Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist; + if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; } + if (!InWater) + { + //lerp the arm downwards when not swimming + dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f); + } + + Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2); + targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength); targetForce = 200.0f; if (target.Submarine != character.Submarine) { @@ -1723,7 +1751,7 @@ namespace Barotrauma { if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X) { - target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; + target.AnimController.LockFlipping(0.5f); } else { @@ -1822,16 +1850,22 @@ namespace Barotrauma public override void Flip() { + if (Character == null || Character.Removed) + { + LogAccessedRemovedCharacterError(); + return; + } + base.Flip(); WalkPos = -WalkPos; Limb torso = GetLimb(LimbType.Torso); - - Vector2 difference; + if (torso == null) { return; } Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation); + Vector2 difference; foreach (Item heldItem in character.HeldItems) { if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent() != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 81e272765..b5e116dab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -57,17 +57,7 @@ namespace Barotrauma { if (limbs == null) { - if (!accessRemovedCharacterErrorShown) - { - string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); - errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce( - "Ragdoll.Limbs:AccessRemoved", - GameAnalyticsManager.ErrorSeverity.Error, - "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); - accessRemovedCharacterErrorShown = true; - } + LogAccessedRemovedCharacterError(); return Array.Empty(); } return limbs; @@ -158,6 +148,20 @@ namespace Barotrauma } } + public bool TryGetCollider(int index, out PhysicsBody collider) + { + collider = null; + try + { + collider = this.collider?[index]; + return true; + } + catch + { + return false; + } + } + public int ColliderIndex { get @@ -873,7 +877,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; } + if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; } limb.Dir = Dir; limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MirrorPullJoint(); @@ -1428,6 +1432,21 @@ namespace Barotrauma return true; } + protected void LogAccessedRemovedCharacterError() + { + if (!accessRemovedCharacterErrorShown) + { + string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); + errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "Ragdoll:AccessRemoved", + GameAnalyticsManager.ErrorSeverity.Error, + "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); + accessRemovedCharacterErrorShown = true; + } + } + partial void UpdateProjSpecific(float deltaTime, Camera cam); partial void Splash(Limb limb, Hull limbHull); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 16657084f..702512ca4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -492,37 +492,52 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); - var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; + var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; + var additionalEffectType = ActionType.OnUse; if (targetCharacter != null && targetCharacter.IsDead) { - effectType = ActionType.OnEating; + additionalEffectType = ActionType.OnEating; } foreach (StatusEffect effect in statusEffects) { effect.sourceBody = sourceBody; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character)) { - // TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied - effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition); + var t = sourceLimb ?? attacker as ISerializableEntity; + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition); + } + effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition); } if (effect.HasTargetType(StatusEffect.TargetType.Parent)) { - effect.Apply(effectType, deltaTime, attacker, attacker); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, attacker, attacker); + } + effect.Apply(additionalEffectType, deltaTime, attacker, attacker); } if (targetCharacter != null) { - if (effect.HasTargetType(StatusEffect.TargetType.Character)) - { - effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter); - } if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb); + } + effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb); } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast().ToList()); + // TODO: do we need the conversion to list here? It generates garbage. + var targets = targetCharacter.AnimController.Limbs.Cast().ToList(); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets); + } + effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets); } } if (target is Entity targetEntity) @@ -532,18 +547,30 @@ namespace Barotrauma { targets.Clear(); effect.AddNearbyTargets(worldPosition, targets); - effect.Apply(effectType, deltaTime, targetEntity, targets); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets); + } + effect.Apply(additionalEffectType, deltaTime, targetEntity, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition); + } + effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition); } } if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { targets.Clear(); targets.AddRange(attacker.Inventory.AllItems); - effect.Apply(effectType, deltaTime, attacker, targets); + if (additionalEffectType != ActionType.OnEating) + { + effect.Apply(conditionalEffectType, deltaTime, attacker, targets); + } + effect.Apply(additionalEffectType, deltaTime, attacker, targets); } } @@ -579,47 +606,52 @@ namespace Barotrauma } var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); - var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; + var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) { effect.sourceBody = sourceBody; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character)) { - effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); + effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); + effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); } if (effect.HasTargetType(StatusEffect.TargetType.Parent)) { - effect.Apply(effectType, deltaTime, attacker, attacker); + effect.Apply(conditionalEffectType, deltaTime, attacker, attacker); + effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker); } - if (effect.HasTargetType(StatusEffect.TargetType.Character)) + if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character); } if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb); } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); + // TODO: do we need the conversion to list here? It generates garbage. + var targets = targetLimb.character.AnimController.Limbs.Cast().ToList(); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); effect.AddNearbyTargets(worldPosition, targets); - effect.Apply(effectType, deltaTime, targetLimb.character, targets); - } - if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) - { - effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition); + effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets); + effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { targets.Clear(); targets.AddRange(attacker.Inventory.AllItems); - effect.Apply(effectType, deltaTime, attacker, targets); + effect.Apply(conditionalEffectType, deltaTime, attacker, targets); + effect.Apply(ActionType.OnUse, deltaTime, attacker, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 44b96063c..5dc7678ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -31,6 +31,9 @@ namespace Barotrauma { public readonly static List CharacterList = new List(); + public const float MaxHighlightDistance = 150.0f; + public const float MaxDragDistance = 200.0f; + partial void UpdateLimbLightSource(Limb limb); private bool enabled = true; @@ -354,9 +357,15 @@ namespace Barotrauma public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + public Identifier Group => Params.Group; + public bool IsHumanoid => Params.Humanoid; + + public bool IsMachine => Params.IsMachine; + public bool IsHusk => Params.Husk; public bool IsMale => info?.IsMale ?? false; @@ -805,6 +814,11 @@ namespace Barotrauma public float HealthPercentage => CharacterHealth.HealthPercentage; public float MaxVitality => CharacterHealth.MaxVitality; public float MaxHealth => MaxVitality; + + /// + /// Was the character in full health at the beginning of the frame? + /// + public bool WasFullHealth => CharacterHealth.WasInFullHealth; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float EmpVulnerability => Params.Health.EmpVulnerability; @@ -1741,7 +1755,7 @@ namespace Barotrauma float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed); targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed); targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed); - SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier); + SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier)); targetMovement *= SpeedMultiplier; // Reset, status effects will set the value before the next update ResetSpeedMultiplier(); @@ -2201,7 +2215,9 @@ namespace Barotrauma if (SelectedCharacter != null) { - if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected) + if (!SelectedCharacter.CanBeSelected || + (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance && + SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance))) { DeselectCharacter(); } @@ -2494,8 +2510,12 @@ namespace Barotrauma if (!skipDistanceCheck) { - maxDist = ConvertUnits.ToSimUnits(maxDist); - if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; } + maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent()); + if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist && + Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist) + { + return false; + } } return checkVisibility ? CanSeeCharacter(c) : true; @@ -3138,56 +3158,57 @@ namespace Barotrauma UpdateAIChatMessages(deltaTime); - //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! - bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true; - bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f; - bool wasRagdolled = false; - bool selfRagdolled = false; - - if (IsForceRagdolled) + if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true) { - IsRagdolled = IsForceRagdolled; - } - else if (this != Controlled) - { - wasRagdolled = IsRagdolled; - IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); - } - //Keep us ragdolled if we were forced or we're too speedy to unragdoll - else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll)) - { - if (ragdollingLockTimer > 0.0f) + bool wasRagdolled = IsRagdolled; + if (IsForceRagdolled) { - SetInput(InputType.Ragdoll, false, true); - ragdollingLockTimer -= deltaTime; + IsRagdolled = IsForceRagdolled; + } + else if (this != Controlled) + { + wasRagdolled = IsRagdolled; + IsRagdolled = IsKeyDown(InputType.Ragdoll); } else { - wasRagdolled = IsRagdolled; - IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; } + bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); + bool bodyMovingTooFast(PhysicsBody body) + { + return + body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || + //falling down counts as going too fast + (!InWater && body.LinearVelocity.Y < -5.0f); + } + if (ragdollingLockTimer > 0.0f) + { + ragdollingLockTimer -= deltaTime; + } + else if (!tooFastToUnragdoll) + { + IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + } + if (IsRagdolled) + { + SetInput(InputType.Ragdoll, false, true); + } } - } - - if (!wasRagdolled && IsRagdolled) - { - if (selfRagdolled) + if (!wasRagdolled && IsRagdolled) { - CheckTalents(AbilityEffectType.OnSelfRagdoll); + CheckTalents(AbilityEffectType.OnRagdoll); } - // currently does not work when you are stunned, like it should - CheckTalents(AbilityEffectType.OnRagdoll); } lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); - //ragdoll button if (IsRagdolled || !CanMove) { if (AnimController is HumanoidAnimController humanAnimController) { humanAnimController.Crouching = false; } + if (IsRagdolled) { AnimController.IgnorePlatforms = true; } AnimController.ResetPullJoints(); SelectedItem = SelectedSecondaryItem = null; return; @@ -3365,6 +3386,20 @@ namespace Barotrauma return distSqr; } + public float GetDistanceToClosestLimb(Vector2 simPos) + { + float closestDist = float.MaxValue; + foreach (Limb limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + float dist = Vector2.Distance(simPos, limb.SimPosition); + dist -= limb.body.GetMaxExtent(); + closestDist = Math.Min(closestDist, dist); + if (closestDist <= 0.0f) { return 0.0f; } + } + return closestDist; + } + private float despawnTimer; private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true) { @@ -3885,8 +3920,8 @@ namespace Barotrauma { foreach (Affliction affliction in attackResult.Afflictions) { - if (affliction.Strength == 0.0f) continue; - sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}"); + if (Math.Abs(affliction.Strength) <= 0.1f) { continue;} + sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}"); } } GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack); @@ -4484,7 +4519,10 @@ namespace Barotrauma #if CLIENT //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns - Inventory?.ApplyReceivedState(); + if (GameMain.Client?.ClientPeer is { IsActive: true }) + { + Inventory?.ApplyReceivedState(); + } #endif base.Remove(); @@ -5200,15 +5238,13 @@ namespace Barotrauma public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier); - /// - /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs - /// public bool IsFriendly(Character other) => IsFriendly(this, other); - /// - /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs - /// - public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other); + + public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); + + public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e65fabba5..49c273024 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -778,9 +778,10 @@ namespace Barotrauma FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); - Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White); - Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); + //default to transparent color, it's invalid and will be replaced with a random one in CheckColors + Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent); + Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent); + Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent); CheckColors(); TryLoadNameAndTitle(npcIdentifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index b9ff8fe06..81b6598ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -602,6 +602,18 @@ namespace Barotrauma break; } } + for (int i = 0; i < effects.Count; i++) + { + for (int j = i + 1; j < effects.Count; j++) + { + var a = effects[i]; + var b = effects[j]; + if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength) + { + DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work."); + } + } + } } public void ClearEffects() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 55e681fb8..4aeeb1ecb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -230,6 +230,11 @@ namespace Barotrauma public float StunTimer { get; private set; } + /// + /// Was the character in full health at the beginning of the frame? + /// + public bool WasInFullHealth { get; private set; } + public Affliction PressureAffliction { get { return pressureAffliction; } @@ -772,6 +777,8 @@ namespace Barotrauma public void Update(float deltaTime) { + WasInFullHealth = vitality >= MaxVitality; + UpdateOxygen(deltaTime); StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; @@ -878,7 +885,11 @@ namespace Barotrauma private void UpdateOxygen(float deltaTime) { - if (!Character.NeedsOxygen) { return; } + if (!Character.NeedsOxygen) + { + oxygenLowAffliction.Strength = 0.0f; + return; + } float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); float prevOxygen = OxygenAmount; @@ -980,6 +991,8 @@ namespace Barotrauma UpdateLimbAfflictionOverlays(); UpdateSkinTint(); Character.Kill(type, affliction); + + WasInFullHealth = false; #if CLIENT DisplayVitalityDelay = 0.0f; DisplayedVitality = Vitality; @@ -1024,17 +1037,18 @@ namespace Barotrauma } private readonly List allAfflictions = new List(); - private List GetAllAfflictions(bool mergeSameAfflictions) + private List GetAllAfflictions(bool mergeSameAfflictions, Func predicate = null) { allAfflictions.Clear(); if (!mergeSameAfflictions) { - allAfflictions.AddRange(afflictions.Keys); + allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate)); } else { foreach (Affliction affliction in afflictions.Keys) { + if (predicate != null && !predicate(affliction)) { continue; } var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab); if (existingAffliction == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b79675fa5..9771ec56e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -200,7 +200,7 @@ namespace Barotrauma } } } - + partial class Limb : ISerializableEntity, ISpatialEntity { //how long it takes for severed limbs to fade out @@ -215,7 +215,7 @@ namespace Barotrauma //the physics body of the limb public PhysicsBody body; - + public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; public Hull Hull; @@ -249,7 +249,7 @@ namespace Barotrauma } } } - + private bool isSevered; private float severedFadeOutTimer; @@ -269,7 +269,7 @@ namespace Barotrauma mouthPos = value; } } - + public readonly Attack attack; public List DamageModifiers { get; private set; } = new List(); @@ -282,39 +282,73 @@ namespace Barotrauma { get { - if (character.AnimController.CurrentAnimationParams is GroundedMovementParams) + if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg) { - switch (type) - { - case LimbType.LeftFoot: - case LimbType.LeftLeg: - case LimbType.LeftThigh: - case LimbType.RightFoot: - case LimbType.RightLeg: - case LimbType.RightThigh: - // Legs always has to flip - return true; - } + // Legs always has to flip when not swimming + return true; } return Params.Flip; } } + public bool DoesMirror + { + get + { + if (IsLeg) + { + // Legs always has to mirror + return true; + } + return DoesFlip; + } + } + public float SteerForce => Params.SteerForce; public Vector2 DebugTargetPos; public Vector2 DebugRefPos; - public bool IsLowerBody => - type == LimbType.LeftLeg || - type == LimbType.RightLeg || - type == LimbType.LeftFoot || - type == LimbType.RightFoot || - type == LimbType.Tail || - type == LimbType.Legs || - type == LimbType.RightThigh || - type == LimbType.LeftThigh || - type == LimbType.Waist; + public bool IsLowerBody + { + get + { + switch (type) + { + case LimbType.LeftLeg: + case LimbType.RightLeg: + case LimbType.LeftFoot: + case LimbType.RightFoot: + case LimbType.Tail: + case LimbType.Legs: + case LimbType.LeftThigh: + case LimbType.RightThigh: + case LimbType.Waist: + return true; + default: + return false; + } + } + } + + public bool IsLeg + { + get + { + switch (type) + { + case LimbType.LeftFoot: + case LimbType.LeftLeg: + case LimbType.LeftThigh: + case LimbType.RightFoot: + case LimbType.RightLeg: + case LimbType.RightThigh: + return true; + default: + return false; + } + } + } public bool IsSevered { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 950684465..1881758e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -75,13 +75,14 @@ namespace Barotrauma.Abilities if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; } switch (wt) { - // it is possible that an item that has both a melee and a projectile component will return true - // even when not used as a melee/ranged weapon respectively - // attackdata should contain data regarding whether the attack is melee or not case WeaponType.Melee: + //if the item has an active projectile component (has been fired), don't consider it a melee weapon + if (item?.GetComponent() is { IsActive: true }) { continue; } if (item?.GetComponent() != null) { return true; } break; case WeaponType.Ranged: + //if the item has a melee weapon component that's being used now, don't consider it a projectile + if (item?.GetComponent() is { Hitting: true }) { continue; } if (item?.GetComponent() != null) { return true; } break; case WeaponType.HandheldRanged: 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 0ac951fd6..c07128f0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -28,7 +28,6 @@ namespace Barotrauma.Abilities conditionals.Add(new PropertyConditional(attribute)); } } - break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index 993c19b94..e4580fadd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -37,19 +37,7 @@ namespace Barotrauma.Abilities if (itemPrefab != null) { - if (category != MapEntityCategory.None) - { - if (!itemPrefab.Category.HasFlag(category)) { return false; } - } - - if (identifiers.Any()) - { - if (!identifiers.Any(t => itemPrefab.Identifier == t)) - { - return false; - } - } - return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + return MatchesItem(itemPrefab); } else { @@ -57,5 +45,22 @@ namespace Barotrauma.Abilities return false; } } + + public bool MatchesItem(ItemPrefab itemPrefab) + { + if (category != MapEntityCategory.None) + { + if (!itemPrefab.Category.HasFlag(category)) { return false; } + } + + if (identifiers.Any()) + { + if (!identifiers.Any(t => itemPrefab.Identifier == t)) + { + return false; + } + } + return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 1b4f880f8..6c4022968 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -17,6 +17,14 @@ namespace Barotrauma.Abilities tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); } + public override void InitializeAbility(bool addingFirstTime) + { + if (addingFirstTime) + { + VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f); + } + } + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { if (conditionsMatched) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs index a1f69c328..26a1d5620 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs @@ -13,6 +13,11 @@ value = abilityElement.GetAttributeFloat("value", 0f); } + public override void InitializeAbility(bool addingFirstTime) + { + VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f); + } + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { if (conditionsMatched != lastState) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs index 78d7f501a..60d7abd61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs @@ -1,19 +1,35 @@ #nullable enable +using Barotrauma.Extensions; using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma.Abilities { internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility { - public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + private readonly AbilityConditionItem? condition; + + public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem)); + if (conditionElement != null) + { + condition = new AbilityConditionItem(CharacterTalent, conditionElement); + } + } protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } - int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); - ingredients.Items.RemoveAt(randomIndex); + List applicableIngredients = condition == null ? + ingredients.Items.ToList() : + ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList(); + if (applicableIngredients.None()) { return; } + + ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index a53d13328..bf290e9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -288,9 +288,7 @@ namespace Barotrauma if (errorCatcher.Errors.Any()) { - yield return ContentPackageManager.LoadProgress.Failure( - ContentPackageManager.LoadProgress.Error - .Reason.ConsoleErrorsThrown); + yield return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text)); yield break; } yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 81056e5eb..88ff41f52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -23,6 +23,8 @@ namespace Barotrauma public const string RegularPackagesElementName = "regularpackages"; public const string RegularPackagesSubElementName = "package"; + public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent); + public static class EnabledPackages { public static CorePackage? Core { get; private set; } = null; @@ -435,22 +437,19 @@ namespace Barotrauma public readonly record struct LoadProgress(Result Result) { public readonly record struct Error( - Error.Reason ErrorReason, - Option Exception) + Either, Exception> ErrorsOrException) { - public enum Reason { Exception, ConsoleErrorsThrown } - - public Error(Reason reason) : this(reason, Option.None) { } - public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { } + public Error(IEnumerable errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { } + public Error(Exception exception) : this(ErrorsOrException: exception) { } } public static LoadProgress Failure(Exception exception) => new LoadProgress( Result.Failure(new Error(exception))); - public static LoadProgress Failure(Error.Reason reason) + public static LoadProgress Failure(IEnumerable errorMessages) => new LoadProgress( - Result.Failure(new Error(reason))); + Result.Failure(new Error(errorMessages))); public static LoadProgress Progress(float value) => new LoadProgress( diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index f2b9dbae6..428050ab2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -88,6 +88,7 @@ namespace Barotrauma public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); + public Range GetAttributeRange(string key, in Range def) => Element.GetAttributeRange(key, def); public Identifier VariantOf() => Element.VariantOf(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 5db14d6ae..509ed5547 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1312,10 +1312,10 @@ namespace Barotrauma } }, () => { - return new[] - { - FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(), - GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + return new[] + { + FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(), + GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() }; }, true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index baa0aecd3..b9695d7dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -49,7 +49,6 @@ namespace Barotrauma OnUseRangedWeapon, OnReduceAffliction, OnAddDamageAffliction, - OnSelfRagdoll, OnRagdoll, OnRoundEnd, OnLootCharacter, @@ -168,7 +167,7 @@ namespace Barotrauma PumpSpeed, PumpMaxFlow, ReactorMaxOutput, - ReactorFuelEfficiency, + ReactorFuelConsumption, DeconstructorSpeed, FabricationSpeed } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 7e4da0ea5..ced3da5c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -226,7 +226,7 @@ namespace Barotrauma bool requiresRescue = element.GetAttributeBool("requirerescue", false); - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos); if (spawnPos is WayPoint wp) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 6e46b3c5b..634652ffa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -97,7 +97,7 @@ namespace Barotrauma List connectedSubs = level.BeaconStation.GetConnectedSubs(); foreach (Item item in Item.ItemList) { - if (!connectedSubs.Contains(item.Submarine)) { continue; } + if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; } if (item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 7b162b544..0ccb6fd13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -404,7 +404,7 @@ namespace Barotrauma { var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); - info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value)); + info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); } // apply money gains afterwards to prevent them from affecting XP gains @@ -545,17 +545,14 @@ namespace Barotrauma return humanPrefab; } - protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true) + protected static Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient) { var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); characterInfo.TeamID = teamType; - if (positionToStayIn == null) - { - positionToStayIn = + positionToStayIn ??= WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ?? WayPoint.GetRandom(SpawnType.Human, null, submarine); - } Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); spawnedCharacter.HumanPrefab = humanPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index d8ccd3753..d4e57fc47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -201,7 +201,7 @@ namespace Barotrauma { continue; } - if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2()))) + if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2())) { removals.Add(position); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index a122a213c..fbb52eaea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -371,7 +371,7 @@ namespace Barotrauma if (!SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent)) + if (ContentPackageManager.ModsEnabled) { message = "[MODDED] " + message; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 791d852b4..2377b386c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -82,7 +82,7 @@ namespace Barotrauma } public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; + public const int MaxMissionCountLimit = 3; public const int MinMissionCountLimit = 1; public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index f28cb563c..fa550b175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -46,15 +46,15 @@ namespace Barotrauma public static void Init() { #if CLIENT - Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true); - DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true); - SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true); - TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true); + Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), isSinglePlayer: true); + DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: true); + SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), isSinglePlayer: true); + TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), isSinglePlayer: true); #endif - Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false); - Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false); - PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false); - MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false); + Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: false); + Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), isSinglePlayer: false); + PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), isSinglePlayer: false); + MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), isSinglePlayer: false); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 569cbac92..c96413662 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -179,14 +179,41 @@ namespace Barotrauma int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); int currentLevel = GetUpgradeLevel(prefab, category); + int newLevel = currentLevel + 1; int maxLevel = prefab.GetMaxLevelForCurrentSub(); if (currentLevel + 1 > maxLevel) { - DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled."); + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled."); return; } + bool TryTakeResources(Character character) + { + bool result = prefab.TryTakeResources(character, newLevel); + if (!result) + { + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources."); + } + return result; + } + + switch (GameMain.NetworkMember) + { + case null when Character.Controlled is { } controlled: // singleplayer + if (!TryTakeResources(controlled)) { return; } + break; + case { IsClient: true }: + if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; } + break; + case { IsServer: true } when client?.Character is { } character: + if (!TryTakeResources(character)) { return; } + break; + default: + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player."); + return; + } + if (price < 0) { Location? location = Campaign.Map?.CurrentLocation; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 40585fad2..bbf59eacf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -188,16 +188,26 @@ namespace Barotrauma.Items.Components private DockingPort FindAdjacentPort() { + float closestDist = float.MaxValue; + DockingPort closestPort = null; foreach (DockingPort port in list) { if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; } - if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) { continue; } - if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) { continue; } + float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X); + if (xDist > DistanceTolerance.X) { continue; } + float yDist = Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y); + if (yDist > DistanceTolerance.Y) { continue; } - return port; + float dist = xDist + yDist; + //disfavor non-interactable ports + if (port.item.NonInteractable) { dist *= 2; } + if (dist < closestDist) + { + closestPort = port; + closestDist = dist; + } } - - return null; + return closestPort; } private void AttemptDock() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index aa26c2e1d..cf33a8eff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -359,7 +359,8 @@ namespace Barotrauma.Items.Components { lastBrokenTime = Timing.TotalTime; //the door has to be restored to 50% health before collision detection on the body is re-enabled - if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f && + (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { IsBroken = false; } @@ -459,7 +460,10 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } - linkedGap.Open = 1.0f; + if (linkedGap != null) + { + linkedGap.Open = 1.0f; + } IsOpen = false; #if CLIENT if (convexHull != null) { convexHull.Enabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 64db689bd..ff4f8b21b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -366,7 +366,6 @@ namespace Barotrauma.Items.Components for (int i = 0; i < entitiesInRange.Count; i++) { float dist = float.MaxValue; - if (entitiesInRange[i] is Structure structure) { if (structure.IsHorizontal) @@ -388,7 +387,7 @@ namespace Barotrauma.Items.Components } else if (entitiesInRange[i] is Character character) { - dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition); + dist = MathF.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition)); } if (dist < closestDist) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index e7d089563..cfb71aca1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -49,6 +49,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Disable to make the weapon ignore all hit effects when it collides with walls, doors, or other items.")] + public bool HitOnlyCharacters + { + get; + set; + } + [Editable, Serialize(true, IsPropertySaveable.No)] public bool Swing { get; set; } @@ -112,7 +119,7 @@ namespace Barotrauma.Items.Components reloadTimer = reload; reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed); reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier); - character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f; + character.AnimController.LockFlipping(); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; @@ -219,7 +226,7 @@ namespace Barotrauma.Items.Components ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); if (ac.InWater) { - ac.LockFlippingUntil = (float)Timing.TotalTime + Reload; + ac.LockFlipping(); } } else @@ -343,33 +350,36 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetCharacter); } - else if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure) + else if (!HitOnlyCharacters) { - if (AllowHitMultiple) + if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure) { - if (hitTargets.Contains(targetStructure)) { return true; } + if (AllowHitMultiple) + { + if (hitTargets.Contains(targetStructure)) { return true; } + } + else + { + if (hitTargets.Any(t => t is Structure)) { return true; } + } + hitTargets.Add(targetStructure); } - else + else if (f2.Body.UserData is Item targetItem) { - if (hitTargets.Any(t => t is Structure)) { return true; } + if (AllowHitMultiple) + { + if (hitTargets.Contains(targetItem)) { return true; } + } + else + { + if (hitTargets.Any(t => t is Item)) { return true; } + } + hitTargets.Add(targetItem); } - hitTargets.Add(targetStructure); - } - else if (f2.Body.UserData is Item targetItem) - { - if (AllowHitMultiple) + else if (f2.Body.UserData is Holdable holdable && holdable.CanPush) { - if (hitTargets.Contains(targetItem)) { return true; } + hitTargets.Add(holdable.Item); } - else - { - if (hitTargets.Any(t => t is Item)) { return true; } - } - hitTargets.Add(targetItem); - } - else if (f2.Body.UserData is Holdable holdable && holdable.CanPush) - { - hitTargets.Add(holdable.Item); } else { @@ -381,6 +391,7 @@ namespace Barotrauma.Items.Components return true; } + private System.Text.StringBuilder serverLogger; private void HandleImpact(Fixture targetFixture) { var target = targetFixture.Body; @@ -398,11 +409,13 @@ namespace Barotrauma.Items.Components Character user = User; Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; + Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure; + Item targetItem = target.UserData as Item; + Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity; if (Attack != null) { Attack.SetUser(user); Attack.DamageMultiplier = damageMultiplier; - if (targetLimb != null) { if (targetLimb.character.Removed) { return; } @@ -415,12 +428,12 @@ namespace Barotrauma.Items.Components targetCharacter.LastDamageSource = item; Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f); } - else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure) + else if (targetStructure != null) { if (targetStructure.Removed) { return; } Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f); } - else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) + else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); @@ -457,27 +470,43 @@ namespace Barotrauma.Items.Components { conditionalActionType = ActionType.OnFailure; } - if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null) + if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb)); - #if SERVER - if (GameMain.Server != null) //TODO: Log structure hits + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity)); + serverLogger ??= new System.Text.StringBuilder(); + serverLogger.Clear(); + serverLogger.Append($"{picker?.LogName} used {item.Name}"); + if (item.ContainedItems != null && item.ContainedItems.Any()) { - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})"); } - #endif + string targetName; + if (targetCharacter != null) + { + targetName = targetCharacter.LogName; + } + else if (targetItem != null) + { + targetName = targetItem.Name; + } + else if (targetStructure != null) + { + targetName = targetStructure.Name; + } + else + { + targetName = targetEntity.ToString(); + } + serverLogger.Append($" on {targetName}."); +#if SERVER + Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack); +#endif } - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? + if (targetEntity != null) { - ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); - ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 0ac7dcac2..774bf603b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -213,6 +213,8 @@ namespace Barotrauma.Items.Components baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); } ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); + ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier); + currentChargeTime = 0f; if (character != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index bf808c48a..1451570d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; @@ -185,24 +184,23 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character); + bool failed = false; if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } - if (UsableIn == UseEnvironment.None) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } - if (item.InWater) { if (UsableIn == UseEnvironment.Air) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } } else @@ -210,9 +208,15 @@ namespace Barotrauma.Items.Components if (UsableIn == UseEnvironment.Water) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); - return false; + failed = true; } } + if (failed) + { + // Always apply ActionType.OnUse. If doesn't fail, the effect is called later. + ApplyStatusEffects(ActionType.OnUse, deltaTime, character); + return false; + } Vector2 rayStart; Vector2 rayStartWorld; @@ -312,9 +316,12 @@ namespace Barotrauma.Items.Components var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them - if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse)) + if (statusEffectLists != null) { - if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f)) + static bool CanSeverJoints(ActionType type, Dictionary> effectList) => + effectList.TryGetValue(type, out List effects) && effects.Any(e => e.SeverLimbsProbability > 0); + + if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists)) { float rangeSqr = ConvertUnits.ToSimUnits(Range); rangeSqr *= rangeSqr; @@ -537,6 +544,7 @@ namespace Barotrauma.Items.Components if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); float structureFixAmount = StructureFixAmount; @@ -605,6 +613,7 @@ namespace Barotrauma.Items.Components } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb); FixCharacterProjSpecific(user, deltaTime, targetCharacter); return true; } @@ -621,6 +630,7 @@ namespace Barotrauma.Items.Components targetLimb.character.LastDamageSource = item; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } @@ -663,6 +673,7 @@ namespace Barotrauma.Items.Components targetItem.IsHighlighted = true; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem); if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) { @@ -875,7 +886,7 @@ namespace Barotrauma.Items.Components } else if (effect.HasTargetType(StatusEffect.TargetType.Character)) { - currentTargets.Add(character); + currentTargets.Add(user); effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index edf91b18e..cd81939f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -210,7 +210,7 @@ namespace Barotrauma.Items.Components if (!(GameMain.NetworkMember is { IsClient: true })) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" - ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); + ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower); } throwState = ThrowState.None; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 01d0dcc71..1643336eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -856,7 +856,13 @@ namespace Barotrauma.Items.Components if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; } if (user != null) { effect.SetUser(user); } effect.AfflictionMultiplier = afflictionMultiplier; - item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); + var c = character; + if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + // A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user. + c = user; + } + item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); effect.AfflictionMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 0499ce20d..b1a12a52c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components //disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll if (character.AnimController is HumanoidAnimController humanoidAnim) { - humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; + humanoidAnim.LockFlipping(0.5f); } if (character.SelectedItem == item) { character.SelectedItem = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index d19f9d7dd..cdf2c9298 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -874,6 +874,6 @@ namespace Barotrauma.Items.Components } private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); - private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate); + private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index d31b52edb..08897d67a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -767,7 +767,7 @@ namespace Barotrauma.Items.Components limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition); return false; } - if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User)) + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) { return false; } @@ -775,7 +775,13 @@ namespace Barotrauma.Items.Components else if (target.Body.UserData is Item item) { if (item.Condition <= 0.0f) { return false; } - if (!item.Prefab.DamagedByProjectiles) { return false; } + if (!item.Prefab.DamagedByProjectiles) + { + if (item.GetComponent() == null) + { + return false; + } + } } else if (target.Body.UserData is Holdable { CanPush: false }) { @@ -882,7 +888,7 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { - if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User)) + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) { return false; } @@ -953,8 +959,8 @@ namespace Barotrauma.Items.Components { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User); + ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User); var attack = targetLimb.attack; if (attack != null) { @@ -965,14 +971,22 @@ namespace Barotrauma.Items.Components { if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); + effect.Apply(effect.type, 1.0f, User, User); + } + if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character); + } + if (effect.HasTargetType(StatusEffect.TargetType.Limb)) + { + effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); effect.AddNearbyTargets(targetLimb.WorldPosition, targets); - effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); + effect.Apply(effect.type, 1.0f, targetLimb.character, targets); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 75426b030..a41e1e1c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -83,7 +83,15 @@ namespace Barotrauma.Items.Components { if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; } - float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f); + float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, level.LevelData.Biome.ActualMaxDifficulty / 100.0f); + + if (level.Type == LevelData.LevelType.Outpost && + level.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + //no high-quality spawns in friendly outposts + difficultyFactor = 0.0f; + } + return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync); static float GetCommonness(int quality, float difficultyFactor) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 7f0c5ca8e..bb38957ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -159,9 +159,11 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + var user = item.GetComponent()?.User; if (source == null || target == null || target.Removed || (source is Entity sourceEntity && sourceEntity.Removed) || - (source is Limb limb && limb.Removed)) + (source is Limb limb && limb.Removed) || + (user != null && user.Removed)) { ResetSource(); target = null; @@ -293,7 +295,6 @@ namespace Barotrauma.Items.Components { targetMass = float.MaxValue; } - var user = item.GetComponent()?.User; if (targetMass > TargetMinMass) { if (Math.Abs(SourcePullForce) > 0.001f) @@ -302,7 +303,7 @@ namespace Barotrauma.Items.Components if (sourceBody != null) { var targetBody = GetBodyToPull(target); - if (targetBody != null && !(targetBody.UserData is Character)) + if (targetBody != null && targetBody.UserData is not Character) { sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 1ccd035f9..6d45bad3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components if (isOn == value && IsActive == value) { return; } IsActive = isOn = value; - SetLightSourceState(value, value ? lightBrightness : 0.0f); + SetLightSourceState(value); OnStateChanged(); } } @@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (Light != null) { - Light.Color = IsOn ? lightColor.Multiply(currentBrightness) : Color.Transparent; + Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent; } #endif } @@ -187,6 +187,8 @@ namespace Barotrauma.Items.Components set; } + public float TemporaryFlickerTimer; + public override void Move(Vector2 amount, bool ignoreContacts = false) { #if CLIENT @@ -205,7 +207,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value, value ? lightBrightness : 0.0f); + SetLightSourceState(value); } } @@ -236,7 +238,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - SetLightSourceState(IsActive, lightBrightness); + SetLightSourceState(IsActive); turret = item.GetComponent(); #if CLIENT if (Screen.Selected.IsEditor) @@ -263,8 +265,7 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - lightBrightness = 1.0f; - SetLightSourceState(true, lightBrightness); + SetLightSourceState(true); SetLightSourceTransformProjSpecific(); base.IsActive = false; isOn = true; @@ -285,13 +286,15 @@ namespace Barotrauma.Items.Components UpdateAITarget(item.AiTarget); } UpdateOnActiveEffects(deltaTime); + //something in UpdateOnActiveEffects may deactivate the light -> return so we don't turn it back on + if (!IsActive) { return; } #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null && !(item.GetRootInventoryOwner() is Character)) + if (item.Container != null && item.GetRootInventoryOwner() is not Character) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); return; } @@ -300,12 +303,14 @@ namespace Barotrauma.Items.Components PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); return; } + TemporaryFlickerTimer -= deltaTime; + //currPowerConsumption = powerConsumption; - if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage)) + if (Rand.Range(0.0f, 1.0f) < 0.05f && (Voltage < Rand.Range(0.0f, MinVoltage) || TemporaryFlickerTimer > 0.0f)) { #if CLIENT if (Voltage > 0.1f) @@ -325,7 +330,7 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); } public override bool Use(float deltaTime, Character character = null) @@ -357,7 +362,7 @@ namespace Barotrauma.Items.Components { LightColor = XMLExtensions.ParseColor(signal.value, false); #if CLIENT - SetLightSourceState(Light.Enabled, currentBrightness); + SetLightSourceState(Light.Enabled); #endif prevColorSignal = signal.value; } @@ -375,7 +380,7 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } - partial void SetLightSourceState(bool enabled, float brightness); + partial void SetLightSourceState(bool enabled, float? brightness = null); public void SetLightSourceTransform() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 863210c6b..09dfe9080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { - partial class WireSection + public partial class WireSection { private Vector2 start; private Vector2 end; @@ -775,20 +775,25 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public static IEnumerable ExtractNodes(XElement element) { - base.Load(componentElement, usePrefabValues, idRemap); - - string nodeString = componentElement.GetAttributeString("nodes", ""); - if (nodeString == "") return; + string nodeString = element.GetAttributeString("nodes", ""); + if (nodeString.IsNullOrWhiteSpace()) { yield break; } string[] nodeCoords = nodeString.Split(';'); for (int i = 0; i < nodeCoords.Length / 2; i++) { - float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x); - float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y); - nodes.Add(new Vector2(x, y)); + float.TryParse(nodeCoords[i * 2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float x); + float.TryParse(nodeCoords[i * 2 + 1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float y); + yield return new Vector2(x, y); } + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + + nodes.AddRange(ExtractNodes(componentElement)); Drawable = nodes.Any(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 3b98d93bc..221ce222a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -445,7 +445,9 @@ namespace Barotrauma.Items.Components { // single charged shot guns will decharge after firing // for cosmetic reasons, this is done by lerping in half the reload time - currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)); + currentChargeTime = reloadTime > 0.0f ? + Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)) : + 0.0f; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 20e17a56d..447b99409 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -268,6 +268,8 @@ namespace Barotrauma get { return capacity; } } + public int EmptySlotCount => slots.Count(i => !i.Empty()); + public bool AllowSwappingContainedItems = true; public Inventory(Entity owner, int capacity, int slotsPerRow = 5) @@ -887,10 +889,7 @@ namespace Barotrauma } if (recursive) { - if (item.OwnInventory != null) - { - item.OwnInventory.FindAllItems(predicate, recursive: true, list); - } + item.OwnInventory?.FindAllItems(predicate, recursive: true, list); } } return list; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c5d74c0d9..628a81168 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -111,6 +111,7 @@ namespace Barotrauma private float sendConditionUpdateTimer; private bool conditionUpdatePending; + private float prevCondition; private float condition; private bool inWater; @@ -1581,7 +1582,7 @@ namespace Barotrauma tags.Add(newTag); } - public IEnumerable GetTags() + public IReadOnlyCollection GetTags() { return tags; } @@ -1743,15 +1744,14 @@ namespace Barotrauma if (Indestructible) { return; } if (InvulnerableToDamage && value <= condition) { return; } - float prev = condition; bool wasInFullCondition = IsFullCondition; condition = MathHelper.Clamp(value, 0.0f, MaxCondition); - if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; } + if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; } RecalculateConditionValues(); - if (condition == 0.0f && prev > 0.0f) + if (condition == 0.0f && prevCondition > 0.0f) { //Flag connections to be updated as device is broken flagChangedConnections(connections); @@ -1765,7 +1765,7 @@ namespace Barotrauma #endif ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } - else if (condition > 0.0f && prev <= 0.0f) + else if (condition > 0.0f && prevCondition <= 0.0f) { //Flag connections to be updated as device is now working again flagChangedConnections(connections); @@ -1793,8 +1793,9 @@ namespace Barotrauma } } - LastConditionChange = condition - prev; + LastConditionChange = condition - prevCondition; ConditionLastUpdated = Timing.TotalTime; + prevCondition = condition; static void flagChangedConnections(Dictionary connections) { @@ -2159,8 +2160,9 @@ namespace Barotrauma var projectile = GetComponent(); if (projectile != null) { - //ignore character colliders (a projectile only hits limbs) - if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; } + // Ignore characters so that the impact sound only plays when the item hits a a wall or a door. + // Projectile collisions are handled in Projectile.OnProjectileCollision(), so it should be safe to do this. + if (f2.CollisionCategories == Physics.CollisionCharacter) { return false; } if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; } if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } } @@ -2711,7 +2713,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); + ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character); if (ic.DeleteOnUse) { remove = true; } } @@ -2742,7 +2744,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character); + ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character); if (ic.DeleteOnUse) { remove = true; } } @@ -2781,8 +2783,8 @@ namespace Barotrauma #endif ic.WasUsed = true; - ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user); - ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user); + ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user); if (GameMain.NetworkMember is { IsServer: true }) { @@ -3448,7 +3450,7 @@ namespace Barotrauma item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); } } - item.lastSentCondition = item.condition; + item.lastSentCondition = item.prevCondition = item.condition; item.RecalculateConditionValues(); item.SetActiveSprite(); @@ -3522,15 +3524,10 @@ namespace Barotrauma upgrade.Save(element); } - if (condition < MaxCondition) - { - element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); - } - else - { - var conditionAttribute = element.GetAttribute("condition"); - if (conditionAttribute != null) { conditionAttribute.Remove(); } - } + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); + + var conditionAttribute = element.GetAttribute("condition"); + if (conditionAttribute != null) { conditionAttribute.Remove(); } parentElement.Add(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 77b03826f..4ad6b2731 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -30,7 +30,7 @@ namespace Barotrauma public EventType EventType { get; } } - public struct ComponentStateEventData : IEventData + public readonly struct ComponentStateEventData : IEventData { public EventType EventType => EventType.ComponentState; public readonly ItemComponent Component; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 83d4a3788..db4471080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -568,9 +568,11 @@ namespace Barotrauma public ImmutableDictionary LevelQuantity { get; private set; } - public bool CanSpriteFlipX { get; private set; } + private bool canSpriteFlipX; + public override bool CanSpriteFlipX => canSpriteFlipX; - public bool CanSpriteFlipY { get; private set; } + private bool canSpriteFlipY; + public override bool CanSpriteFlipY => canSpriteFlipY; /// /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. @@ -884,8 +886,8 @@ namespace Barotrauma case "sprite": string spriteFolder = GetTexturePath(subElement, variantOf); - CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); - CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); + canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); + canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); if (subElement.GetAttribute("sourcerect") == null && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index c05c9517a..b22a9280e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -188,6 +188,12 @@ namespace Barotrauma item.Condition -= item.MaxCondition * EmpStrength * distFactor; } + var lightComponent = item.GetComponent(); + if (lightComponent != null) + { + lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f); + } + //discharge batteries var powerContainer = item.GetComponent(); if (powerContainer != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index eec99f20b..72bbace41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -860,6 +860,7 @@ namespace Barotrauma (endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) || (endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; } if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; } + if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; } potentialIslands.Add(cell); } for (int i = 0; i < GenerationParams.IslandCount; i++) @@ -1099,6 +1100,7 @@ namespace Barotrauma caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells)); foreach (var caveCell in caveCells) { + if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; } if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) { var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f); @@ -3176,7 +3178,7 @@ namespace Barotrauma TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient); - if (!cells.Any(c => c.IsPointInside(startPos + offset))) + if (!IsPositionInsideWall(startPos + offset)) { startPos += offset; } @@ -3232,10 +3234,9 @@ namespace Barotrauma { suitablePositions.RemoveAll(p => !filter(p)); } - //avoid floating ice chunks on the main path if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) { - suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2())))); + suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); } if (!suitablePositions.Any()) { @@ -3288,10 +3289,16 @@ namespace Barotrauma return false; } - position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; + position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position; return true; } + public bool IsPositionInsideWall(Vector2 worldPosition) + { + var closestCell = GetClosestCell(worldPosition); + return closestCell != null && closestCell.IsPointInside(worldPosition); + } + public void Update(float deltaTime, Camera cam) { LevelObjectManager.Update(deltaTime); @@ -3334,14 +3341,13 @@ namespace Barotrauma public Vector2 GetBottomPosition(float xPosition) { - int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1)); + float interval = Size.X / (bottomPositions.Count - 1); + + int index = (int)Math.Floor(xPosition / interval); if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } - float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); - //t can go slightly outside the 0-1 due to rounding, safe to ignore - Debug.Assert(t <= 1.001f && t >= -0.001f); + float t = (xPosition - bottomPositions[index].X) / interval; t = MathHelper.Clamp(t, 0.0f, 1.0f); - float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); return new Vector2(xPosition, yPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 0d1956a5e..df2c0679c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -171,7 +171,7 @@ namespace Barotrauma } int startLocationindex = element.GetAttributeInt("startlocation", -1); - if (startLocationindex > 0 && startLocationindex < Locations.Count) + if (startLocationindex >= 0 && startLocationindex < Locations.Count) { StartLocation = Locations[startLocationindex]; } @@ -188,7 +188,7 @@ namespace Barotrauma } } int endLocationindex = element.GetAttributeInt("endlocation", -1); - if (endLocationindex > 0 && endLocationindex < Locations.Count) + if (endLocationindex >= 0 && endLocationindex < Locations.Count) { EndLocation = Locations[endLocationindex]; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 6d9077cc0..2c9624004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -174,6 +174,9 @@ namespace Barotrauma public abstract Sprite Sprite { get; } + public virtual bool CanSpriteFlipX { get; } = false; + public virtual bool CanSpriteFlipY { get; } = false; + public abstract string OriginalName { get; } public abstract LocalizedString Name { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 9c76e0bb2..414c4c8da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -124,9 +124,18 @@ namespace Barotrauma int eventCount = GameMain.Server.EntityEventManager.Events.Count(); int uniqueEventCount = GameMain.Server.EntityEventManager.UniqueEvents.Count(); #endif - List entities = MapEntity.mapEntityList.FindAll(e => e.Submarine == sub); + HashSet connectedSubs = new HashSet() { sub }; + foreach (Submarine otherSub in Submarine.Loaded) + { + //remove linked subs too + if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); } + } + List entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine)); entities.ForEach(e => e.Remove()); - sub.Remove(); + foreach (Submarine otherSub in connectedSubs) + { + otherSub.Remove(); + } #if SERVER //remove any events created during the removal of the entities GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 3dd5eea35..18e48a4c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -20,8 +20,8 @@ namespace Barotrauma public readonly ContentXElement ConfigElement; - public readonly bool CanSpriteFlipX; - public readonly bool CanSpriteFlipY; + public override bool CanSpriteFlipX { get; } + public override bool CanSpriteFlipY { get; } /// /// If null, the orientation is determined automatically based on the dimensions of the structure instances diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index e2958efdf..d5f4e7387 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1310,7 +1310,7 @@ namespace Barotrauma return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); } - public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) + public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) { upgradeEventIdentifier = new Identifier($"Submarine{ID}"); Loading = true; @@ -1381,65 +1381,65 @@ namespace Barotrauma center.Y -= center.Y % GridSize.Y; RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); + } - subBody = new SubmarineBody(this, showWarningMessages); - Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); - subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); + subBody = new SubmarineBody(this, showErrorMessages); + Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); + subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); - if (info.IsOutpost) + if (info.IsOutpost) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + TeamID = CharacterTeamType.FriendlyNPC; + + bool indestructible = + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && + !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + + foreach (MapEntity me in MapEntity.mapEntityList) { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = CharacterTeamType.FriendlyNPC; - - bool indestructible = - GameMain.NetworkMember != null && - !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && - !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); - - foreach (MapEntity me in MapEntity.mapEntityList) + if (me.Submarine != this) { continue; } + if (me is Item item) { - if (me.Submarine != this) { continue; } - if (me is Item item) + item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; + item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; + if (item.GetComponent() != null && indestructible) { - item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; - item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; - if (item.GetComponent() != null && indestructible) + item.Indestructible = true; + } + foreach (ItemComponent ic in item.Components) + { + if (ic is ConnectionPanel connectionPanel) { - item.Indestructible = true; - } - foreach (ItemComponent ic in item.Components) - { - if (ic is ConnectionPanel connectionPanel) + //prevent rewiring + if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) { - //prevent rewiring - if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) - { - connectionPanel.Locked = true; - } + connectionPanel.Locked = true; } - else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) - { - //prevent deattaching items from walls + } + else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) + { + //prevent deattaching items from walls #if CLIENT if (GameMain.GameSession?.GameMode is TutorialMode) { continue; } #endif - holdable.CanBePicked = false; - holdable.CanBeSelected = false; - } + holdable.CanBePicked = false; + holdable.CanBeSelected = false; } } - else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) - { - structure.Indestructible = true; - } + } + else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) + { + structure.Indestructible = true; } } - else if (info.IsRuin) - { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - } + } + else if (info.IsRuin) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; } if (entityGrid != null) @@ -1481,7 +1481,7 @@ namespace Barotrauma #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula - if (showWarningMessages && + if (showErrorMessages && !string.IsNullOrEmpty(Info.FilePath) && Screen.Selected != GameMain.SubEditorScreen && (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index fa7470faa..7867f1166 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -109,7 +109,7 @@ namespace Barotrauma get { return submarine; } } - public SubmarineBody(Submarine sub, bool showWarningMessages = true) + public SubmarineBody(Submarine sub, bool showErrorMessages = true) { this.submarine = sub; @@ -119,9 +119,9 @@ namespace Barotrauma if (!Hull.HullList.Any(h => h.Submarine == sub)) { farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f); - if (showWarningMessages) + if (showErrorMessages) { - DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed."); + DebugConsole.ThrowError($"No hulls found in the submarine \"{sub.Info.Name}\". Generating a physics body for the submarine failed."); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 93a7b09eb..d453464f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Networking public T GetVote(VoteType voteType) { - return (votes[(int)voteType] is T) ? (T)votes[(int)voteType] : default(T); + return (votes[(int)voteType] is T t) ? t : default; } public void SetVote(VoteType voteType, object value) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs index 1547b1b5a..cf944004e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs @@ -34,7 +34,7 @@ interface IServerPositionSync : IServerSerializable { #if SERVER - void ServerWritePosition(IWriteMessage msg, Client c); + void ServerWritePosition(ReadWriteMessage tempBuffer, Client c); #endif #if CLIENT void ClientReadPosition(IReadMessage msg, float sendingTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index ac4b80df1..a1ce193dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -99,6 +99,19 @@ namespace Barotrauma.Networking EntityEventInitial } + [NetworkSerialize] + readonly record struct EntityPositionHeader( + bool IsItem, + UInt32 PrefabUintIdentifier, + UInt16 EntityId) : INetSerializableStruct + { + public static EntityPositionHeader FromEntity(Entity entity) + => new ( + IsItem: entity is Item, + PrefabUintIdentifier: entity is MapEntity me ? me.Prefab.UintIdentifier : 0, + EntityId: entity.ID); + } + enum TraitorMessageType { Server, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index bcc5c90b9..02fc9f386 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -171,6 +171,8 @@ namespace Barotrauma.Networking public bool ShouldCreateAnalyticsEvent => DisconnectReason is not ( DisconnectReason.Disconnected + or DisconnectReason.ServerShutdown + or DisconnectReason.ServerFull or DisconnectReason.Banned or DisconnectReason.Kicked or DisconnectReason.TooManyFailedLogins diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index e4cbc1bb7..01cbd8d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -1093,7 +1093,7 @@ namespace Barotrauma.Networking for (int i = 0; i < count; i++) { int index = msg.ReadUInt16(); - if (index < 0 || index >= subList.Count) { continue; } + if (index >= subList.Count) { continue; } string submarineName = subList[index].Name; HiddenSubs.Add(submarineName); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 594171239..14ae70d17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 }; - private IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) + private static IReadOnlyDictionary GetVoteCounts(VoteType voteType, IEnumerable voters) { Dictionary voteList = new Dictionary(); @@ -29,7 +29,7 @@ namespace Barotrauma return voteList; } - public T HighestVoted(VoteType voteType, List voters) + public static T HighestVoted(VoteType voteType, IEnumerable voters) { if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; } if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 09e78f943..8108d7fe8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -621,6 +621,15 @@ namespace Barotrauma return stringValue.Split(';').Select(s => ParseTuple(s, default)).ToArray(); } + public static Range GetAttributeRange(this XElement element, string name, Range defaultValue) + { + var attribute = element?.GetAttribute(name); + if (attribute is null) { return defaultValue; } + + string stringValue = attribute.Value; + return string.IsNullOrEmpty(stringValue) ? defaultValue : ParseRange(stringValue); + } + public static string ElementInnerText(this XElement el) { StringBuilder str = new StringBuilder(); @@ -895,6 +904,37 @@ namespace Barotrauma return floatArray; } + // parse a range string, e.g "1-3" or "3" + public static Range ParseRange(string rangeString) + { + if (string.IsNullOrWhiteSpace(rangeString)) { return GetDefault(rangeString); } + + string[] split = rangeString.Split('-'); + return split.Length switch + { + 1 when TryParseInt(split[0], out int value) => new Range(value, value), + 2 when TryParseInt(split[0], out int min) && TryParseInt(split[1], out int max) && min < max => new Range(min, max), + _ => GetDefault(rangeString) + }; + + static bool TryParseInt(string value, out int result) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result); + } + + result = default; + return false; + } + + static Range GetDefault(string rangeString) + { + DebugConsole.ThrowError($"Error parsing range: \"{rangeString}\" (using default value 0-99)"); + return new Range(0, 99); + } + } + public static Identifier VariantOf(this XElement element) => element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", "")); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index b327c3903..5776bc9e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -64,7 +64,6 @@ namespace Barotrauma EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, - EditorDisclaimerShown = false, ShowOffensiveServerPrompt = true, TutorialSkipWarning = true, CorpseDespawnDelay = 600, @@ -132,7 +131,6 @@ namespace Barotrauma public EnemyHealthBarMode ShowEnemyHealthBars; public bool ChatOpen; public bool CrewMenuOpen; - public bool EditorDisclaimerShown; public bool ShowOffensiveServerPrompt; public bool TutorialSkipWarning; public int CorpseDespawnDelay; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 301137e0a..b241816f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -75,6 +75,7 @@ namespace Barotrauma case "targetgrandparent": case "targetcontaineditem": case "skillrequirement": + case "targetslot": return false; default: return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 2d9b3723c..c891bb02d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1760,12 +1760,17 @@ namespace Barotrauma void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) { + Item parentItem = entity as Item; + if (user == null && parentItem != null) + { + // Set the user for projectiles spawned from status effects (e.g. flak shrapnels) + SetUser(parentItem.GetComponent()?.User); + } switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { - Item parentItem = entity as Item; Projectile projectile = newItem.GetComponent(); if (entity != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index d9dd7a1eb..0cd9799f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -85,15 +85,6 @@ namespace Barotrauma.Steam return Steamworks.SteamUGC.NumSubscribedItems; } - public static PublishedFileId[] GetSubscribedItems() - { - if (!IsInitialized || !Steamworks.SteamClient.IsValid) - { - return Array.Empty(); - } - return Steamworks.SteamUGC.GetSubscribedItems(); - } - public static bool UnlockAchievement(string achievementIdentifier) => UnlockAchievement(achievementIdentifier.ToIdentifier()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index c9cde9d90..071f08337 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -1,26 +1,20 @@ #nullable enable using Barotrauma.IO; using System; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.Extensions; -using Steamworks.Data; using WorkshopItemSet = System.Collections.Generic.ISet; namespace Barotrauma.Steam { static partial class SteamManager { - public const string WorkshopItemPreviewImageFolder = "Workshop"; - public const string PreviewImageName = "PreviewImage.png"; - public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; - public static bool TryExtractSteamWorkshopId(this ContentPackage contentPackage, [NotNullWhen(true)]out SteamWorkshopId? workshopId) { workshopId = null; @@ -47,16 +41,22 @@ namespace Barotrauma.Steam private static async Task GetWorkshopItems(Steamworks.Ugc.Query query, int? maxPages = null) { if (!IsInitialized) { return new HashSet(); } - + await Task.Yield(); - query = query.WithKeyValueTags(true).WithLongDescription(true); var set = new HashSet(ItemEqualityComparer.Instance); int prevSize = 0; - for (int i = 1; maxPages is null || i <= maxPages; i++) + for (int i = 1; i <= (maxPages ?? int.MaxValue); i++) { - Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i); - if (page is null || !page.Value.Entries.Any()) { break; } - set.UnionWith(page.Value.Entries); + using Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i); + if (page is not { Entries: var entries }) { break; } + + // This queries the results on the i-th page and stores them, + // using page.Entries directly would result in two GetQueryUGCResult calls + entries = entries.ToArray(); + + if (entries.None()) { break; } + + set.UnionWith(entries); if (set.Count == prevSize) { break; } prevSize = set.Count; @@ -66,10 +66,17 @@ namespace Barotrauma.Steam // which can happen on items that are not visible to the currently // logged in player (i.e. private & friends-only items) set.RemoveWhere(it => it.ConsumerApp != AppID); - + return set; } + public static ImmutableHashSet GetSubscribedItemIds() + { + return IsInitialized + ? Steamworks.SteamUGC.GetSubscribedItems().ToImmutableHashSet() + : ImmutableHashSet.Empty; + } + public static async Task GetAllSubscribedItems() { if (!IsInitialized) { return new HashSet(); } @@ -98,14 +105,86 @@ namespace Barotrauma.Steam .WhereUserPublished()); } - public static async Task GetItem(UInt64 itemId) + private static class SingleItemRequestPool + { + private static readonly object mutex = new(); + private static readonly TimeSpan delayAfterNewRequest = TimeSpan.FromSeconds(0.5); + private static readonly HashSet ids = new(); + + private static Task? currentBatch = null; + + private static async Task PrepareNewBatch() + { + // Wait for a bunch of requests to be made + await Task.Delay(delayAfterNewRequest); + + Task queryTask; + lock (mutex) + { + DebugConsole.Log( + $"{nameof(SteamManager)}.{nameof(Workshop)}.{nameof(SingleItemRequestPool)}: " + + $"Running batch of {ids.Count} requests"); + + queryTask = GetWorkshopItems( + Steamworks.Ugc.Query.All + .WithFileId( + ids + .Select(id => (Steamworks.Data.PublishedFileId)id) + .ToArray())); + ids.Clear(); + + // Immediately clear the current batch so the next request starts a new one + currentBatch = null; + } + + return await queryTask; + } + + public static async Task MakeRequest(UInt64 id) + { + Task ourTask; + lock (mutex) + { + ids.Add(id); + if (currentBatch is not { IsCompleted: false }) + { + // There is no currently pending batch, start a new one + currentBatch = Task.Run(PrepareNewBatch); + } + ourTask = currentBatch; + } + + var items = await ourTask; + var result = items.FirstOrNull(it => it.Id == id); + return result; + } + } + + /// + /// Fetches a Workshop item's metadata. This is batched to minimize Steamworks API calls. + /// The description of the returned item is truncated to save bandwidth. + /// + /// Workshop Item ID + public static Task GetItem(UInt64 itemId) + => SingleItemRequestPool.MakeRequest(itemId); + + /// + /// Fetches a Workshop item's metadata in its own API call instead of batching. + /// This minimizes delay but needs to be used with caution to prevent rate limiting. + /// + /// Workshop Item ID + /// + /// If true, ask for the item's entire description, otherwise it'll be truncated. + /// + public static async Task GetItemAsap(UInt64 itemId, bool withLongDescription = false) { if (!IsInitialized) { return null; } var items = await GetWorkshopItems( Steamworks.Ugc.Query.All - .WithFileId(itemId)); - return items.Any() ? items.First() : (Steamworks.Ugc.Item?)null; + .WithFileId(itemId) + .WithLongDescription(withLongDescription)); + return items.Any() ? items.First() : null; } public static async Task ForceRedownload(UInt64 itemId) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 84b97b6b0..6edb6b94f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -257,10 +257,59 @@ namespace Barotrauma } } - internal partial class UpgradePrefab : UpgradeContentPrefab + internal readonly struct UpgradeResourceCost + { + public readonly int Amount; + private readonly ImmutableArray targetTags; + public readonly Range TargetLevels; + + public UpgradeResourceCost(ContentXElement element) + { + Amount = element.GetAttributeInt("amount", 0); + targetTags = element.GetAttributeIdentifierArray("item", Array.Empty())!.ToImmutableArray(); + TargetLevels = element.GetAttributeRange("levels", new Range(0, 99)); + } + + public bool AppliesForLevel(int currentLevel) => TargetLevels.Contains(currentLevel); + + public bool AppliesForLevel(Range newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start; + + public bool MatchesItem(Item item) => MatchesItem(item.Prefab); + + public bool MatchesItem(ItemPrefab item) + { + foreach (Identifier tag in targetTags) + { + if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) { return true; } + } + + return false; + } + } + + internal readonly struct ApplicableResourceCollection + { + public readonly ImmutableArray MatchingItems; + public readonly UpgradeResourceCost Cost; + public readonly int Count; + + public ApplicableResourceCollection(IEnumerable matchingItems, int count, UpgradeResourceCost cost) + { + MatchingItems = matchingItems.ToImmutableArray(); + Count = count; + Cost = cost; + } + + public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost) + { + return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost); + } + } + + internal sealed partial class UpgradePrefab : UpgradeContentPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection( - onAdd: (prefab, isOverride) => + onAdd: static (prefab, isOverride) => { if (!prefab.SuppressWarnings && !isOverride) { @@ -329,6 +378,7 @@ namespace Barotrauma private Dictionary targetProperties { get; } private readonly ImmutableArray MaxLevelsMods; + public readonly ImmutableHashSet ResourceCosts; public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { @@ -341,6 +391,7 @@ namespace Barotrauma var targetProperties = new Dictionary(); var maxLevels = new List(); + var resourceCosts = new HashSet(); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); if (!nameIdentifier.IsEmpty) @@ -383,6 +434,11 @@ namespace Barotrauma maxLevels.Add(new UpgradeMaxLevelMod(subElement)); break; } + case "resourcecost": + { + resourceCosts.Add(new UpgradeResourceCost(subElement)); + break; + } #if CLIENT case "decorativesprite": { @@ -414,6 +470,7 @@ namespace Barotrauma this.targetProperties = targetProperties; MaxLevelsMods = maxLevels.ToImmutableArray(); + ResourceCosts = resourceCosts.ToImmutableHashSet(); upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? .ToImmutableHashSet() ?? ImmutableHashSet.Empty; @@ -456,6 +513,58 @@ namespace Barotrauma return GetMaxLevel(info) > 0; } + public bool HasResourcesToUpgrade(Character? character, int currentLevel) + { + if (character is null) { return false; } + if (!ResourceCosts.Any()) { return true; } + + List allItems = character.Inventory.FindAllItems(recursive: true); + return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem)); + } + + public bool TryTakeResources(Character character, int currentLevel) + { + IEnumerable costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); + + if (!costs.Any()) { return true; } + + List allItems = character.Inventory.FindAllItems(recursive: true); + HashSet itemsToRemove = new HashSet(); + + foreach (UpgradeResourceCost cost in costs) + { + int amountNeeded = cost.Amount; + foreach (Item item in allItems.Where(cost.MatchesItem)) + { + itemsToRemove.Add(item); + amountNeeded--; + if (amountNeeded <= 0) { break; } + } + + if (amountNeeded > 0) { return false; } + } + + foreach (Item item in itemsToRemove) + { + item.Remove(); + } + + if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); } + + return true; + } + + public ImmutableArray GetApplicableResources(int level) + { + var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet(); + + var costs = applicableCosts.Any() + ? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray() + : ImmutableArray.Empty; + + return costs; + } + public bool IsDisallowed(MapEntity item) { return item.DisallowedUpgradeSet.Contains(Identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index 9fdc1e786..c4ce599c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -212,8 +212,13 @@ namespace Barotrauma return false; } + public override int GetHashCode() + { + return ShortRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase); + } + public static bool operator ==(Md5Hash? a, Md5Hash? b) - => (a is null == b is null) && (a?.Equals(b) ?? true); + => Equals(a, b); public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs index eaf89a4f1..9a5393a32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } - public bool Contains(in T v) + public readonly bool Contains(in T v) => start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0; private void VerifyStartLessThanEnd() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index d6cc6a92f..20e5f3290 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -11,10 +11,10 @@ namespace Barotrauma public abstract bool IsSuccess { get; } public bool IsFailure => !IsSuccess; - public static Success Success(T value) + public static Result Success(T value) => new Success(value); - public static Failure Failure(TError error) + public static Result Failure(TError error) => new Failure(error); public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs index 63f652f57..556ca0fd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Networking; */ [NetworkSerialize] -public readonly record struct Segment(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct; +public readonly record struct Segment(T Identifier, int Pointer) : INetSerializableStruct where T : struct; readonly ref struct SegmentTableWriter where T : struct { @@ -94,7 +94,7 @@ readonly ref struct SegmentTableWriter where T : struct public static SegmentTableWriter StartWriting(IWriteMessage msg) { var retVal = new SegmentTableWriter(msg, msg.BitPosition); - msg.WriteUInt16(0); //reserve space for the table pointer + msg.WriteInt32(0); //reserve space for the table pointer return retVal; } @@ -104,28 +104,22 @@ readonly ref struct SegmentTableWriter where T : struct { throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>"); } - - if (message.BitPosition - PointerLocation > UInt16.MaxValue) - { - throw new OverflowException( - $"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)"); - } } - + public void StartNewSegment(T value) { ThrowOnInvalidState(); - segments.Add(new Segment(value, (UInt16)(message.BitPosition-PointerLocation))); + segments.Add(new Segment(value, message.BitPosition - PointerLocation)); } public void Dispose() { ThrowOnInvalidState(); int tablePosition = message.BitPosition; - + //rewrite the table pointer now that we know where the table ends message.BitPosition = PointerLocation; - message.WriteUInt16((UInt16)(tablePosition-PointerLocation)); + message.WriteInt32(tablePosition - PointerLocation); //write the table message.BitPosition = tablePosition; @@ -274,7 +268,7 @@ readonly ref struct SegmentTableReader where T : struct ExceptionHandler? exceptionHandler = null) { int pointerLocation = msg.BitPosition; - int tablePointer = msg.ReadUInt16(); + int tablePointer = msg.ReadInt32(); int tableLocation = pointerLocation + tablePointer; int returnPosition = msg.BitPosition; diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 60923fafe..cd2fbd47e 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,104 @@ +--------------------------------------------------------------------------------------------------------- +v0.21.1.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Changes: +- Added translations for the submarine and character editors. +- Some submarine upgrades cost materials in addition to money. +- Miscellaneous fixes to all translations. +- The "max missions" campaign setting is restricted to a maximum of 3. +- Minor improvements to ragdoll animation when falling prone due to stun/unconsciousness/ragdolling. +- Revisited the skill requirements and the OnFailure conditions on welding tool, plasma cutter, flamer, and steam gun. Flamer and steam gun now apply burns on the user only when they don't have enough skills to use the item. All these items are now less efficient when used with low skills. +- Changed Alarm Buzzer sound to differentiate it from the diving suit warning beep. +- Depth charge tweaks: require UEX, increase non-contained item explosion radius and damage, allow quality to affect explosion, increase pricing. +- Reduceed Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular. +- Lights flicker when hit by EMP. +- Large monsters no longer drop the loot when they die. This was implemented as a workaround to it being difficult to grab large monsters, but now they're much easier to grab - you can grab them anywhere near their main limb, instead of having to find the "origin" somewhere at the center of the monster. + +Talents: +- Fixed "Down With the Ship" sometimes having an incorrect description. +- Fixed "Bounty Hunter" and "Logistics Expert" not giving 15% experience bonus. +- Pet raptor eggs can be fabricated from raptor eggs of any size. +- Fixed Flamer being craftable by anyone, even though it should only be possible with the "Pyromaniac" talent. +- Fixed ability to move in an unpowered exosuit when you've got any speed buff. +- Fixed "True Potential" and "Chonky Honks" not requiring clown power to work. +- Reduced the chance of finding genetic materials with the "Gene Harvester" talent, fixed genetic materials sometimes being found on defense bots. +- Fixed "Spec Ops" not working properly with shotguns and other weapons that fire multiple projectiles per shot. Only the first projectile that hit did double damage. +- Fixed some parts of Hemulen and Venture not flooding properly. +- "Mass production" talent only applies to items in the "material" category. Fixes e.g. recycle recipes sometimes allowing you to keep the original item. +- Fixed "Lone Wolf" not giving any damage/stun resistance. +- Fixed "Scrap Savant" having a 80% chance of finding scrap instead of 20%. +- Fixed "Steady Tune" not doing what the description says (giving a constantly diminishing 7.5% resistance instead of 60%), made the talent give 100% immunity instead. +- Fixed "Multifunctional" talent not giving a boost to crowbar damage. +- Fixed inaccurate "Unstoppable Curiosity" and "Ph.D in Nuclear Physics" descriptions. + +Multiplayer: +- Better support for playing the MP campaign without a host or someone with campaign management permissions on the server: + - You can vote to end the round in the campaign too. + - Automatic restart works in the campaign mode too. + - Anyone can manage salaries if there's no-one allowed to do it. + - If there's no host or anyone with permissions in the server, anyone is able to setup a new campaign. + - Campaign can be voted for when game mode voting is enabled. +- Made ragdoll syncing more robust: reduces cases of teleportation/desync when manually ragdolling the character in multiplayer. +- Fixed rejoining clients not regaining control of their character even if the character is still alive, if the client's IP address has changed. A respawn would also not trigger. +- Fixed "failed to write a network message, too much data is being stored in SegmentTable" errors that could occur in various situations: for example when the host has a large ban list, lots of submarines, and when rewiring a device with lots of connections. +- Fixed "Create new character" button not appearing in the tab menu when dead or spectating. +- Fixed clients not entering the server lobby if they accept a server invite during a single player round or tutorial. +- Fixed inability to join IPv4 servers when IPv6 is disabled. +- Fixed hidden submarine list sometimes desyncing if you have specific custom submarines. + +Bugfixes: +- Fixed high-quality items spawning earlier in the campaign when playing with a higher campaign difficulty setting. +- Fixed attacking with a melee weapon making you unable to turn (flip) for a while. +- Fixed ragdolling affecting the character's velocity, allowing it to be used as a way to avoid fall damage. +- Hull fixes to vanilla subs and wrecks. +- Fixed alien flares practically never spawning in ruins. +- Fixed status effects defined inside an attack definition still using the old OnUse/OnFailure logic instead of OnSuccess/OnFailure. +- Fixed "save as item assembly" and "snap to grid" buttons taking cursor focus in the sub editor even when they're not visible. +- Fixed inability to launch custom dedicated server executables from the main menu on mac and linux. +- Fixed inability to drag and drop items from the entity list to small containers (such as battery charging docks) in the sub editor. +- Fixed item condition not decreasing client-side if the condition decreases very slowly: for example when using a thorium rod with the "Cruisin'" talent. +- Fixed flares still emitting light after running out. +- Fixed Electrical Discharge Coil preview not working in the sub editor. +- Fixed alien flares not activating when clicking LMB. +- Fixed crawler's arms getting broken when the character flips in water. +- Fixed the recycle recipe of flak explosive ammo. +- Fixed misaligned shells in wrecked railgun shell rack. +- Fixed misaligned light component light sprite. +- Fixed crashing if the select audio device is disconnected while in the initial loading screen. +- Fixed inability to sell genetic materials that aren't 100% refined. +- Fixed liquid oxygenite exploding too easily. + +Modding: +- Addressed various inconsistencies, issues and limitations in how status effects are used in certain cases: + - Status effects defined for attacks with the type "UseTarget" now correctly target the use target, instead of the attacker like they used to. + - Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon and Projectile components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons. + - MODDERS, PLEASE NOTE: Effects that target "Character" in the previously mentioned components now affect the user - if that's not the intention, the target should be changed to "UseTarget". + - The use target can now be a character, an item, or a structure, depending on the context. This allows effects that weren't previously possible, but due to it we'll now need to introduce some restrictions in the definitions in some cases. For example, we might want to use a conditional to check whether the target is of the right type, before triggering the status effect (). + - Added a new attribute for the MeleeWeapon component, "HitOnlyCharacters", which can be used for ignoring the hits to walls and items entirely. + - Due to the changes, some status effects that previously worked, might now need the "AllowWhenBroken" set to true in the definition to keep them working as they used to. So e.g. the "OnImpact" doesn't work anymore on your custom explosive, try that. +- Fixed crashing when trying to place a wreck with no hulls in a level. +- Fixed mod descriptions getting truncated to 255 characters when selecting an already-published item in the Mods menu. +- Fixed HMG's requiring hmgmagazine instead of any item with the type "hmgammo", making the use of modded ammo impossible without overriding the gun too. + +--------------------------------------------------------------------------------------------------------- +v0.20.16.1 +--------------------------------------------------------------------------------------------------------- + +- Fixed console errors when firing a Flak Cannon using spreader ammo in multiplayer. + +--------------------------------------------------------------------------------------------------------- +v0.20.16.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed non-hitscan projectiles going through doors. +- Fixed electrical discharge coils hitting characters very unreliably, unless the character happens to be right next to a wall. +- Fixed makeshift shelves being containable in crates and cabinets, allowing for infinite recursive storage space. +- Fixed the effects of the "Grid Maintainer" and "Egghead" talents. +- Fixed incorrect "I Am That Guy" description (it gives a flat 20 skill bonus, not 20%). +- Fixed "Cruisin'" talent increasing fuel consumption by 10% instead of decreasing it by 20%. +- Optimized Steam Workshop queries done by the game (less bandwidth usage and stress on Steam's servers). + --------------------------------------------------------------------------------------------------------- v0.20.15.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs index f0bd29a02..86213b54d 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; using Barotrauma; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Xunit; @@ -11,6 +13,61 @@ namespace TestProject; public class INetSerializableStructImplementationChecks { private delegate bool TryFindBehaviorDelegate(Type type, out NetSerializableProperties.IReadWriteBehavior behavior); + + private Type FillGenericParameters(Type type) + { + // Plug in some known good parameters to evaluate + // a concrete instance of this generic type + + var paramsConstraints = type.GetGenericArguments() + .Select(p => p.GetGenericParameterConstraints()) + .ToImmutableArray(); + + var chosenArgs = new Type[paramsConstraints.Length]; + + for (int i = 0; i < paramsConstraints.Length; i++) + { + var constraints = paramsConstraints[i]; + var baseTypeConstraints = constraints.Where(c => !c.IsGenericParameter); + + bool hasGenericConstraint(GenericParameterAttributes flag) + => constraints.Any(c + => c.IsGenericParameter && c.GenericParameterAttributes.HasFlag(flag)); + + bool refTypeConstraint = hasGenericConstraint(GenericParameterAttributes.ReferenceTypeConstraint); + bool valueTypeConstraint = baseTypeConstraints.Contains(typeof(ValueType)); + + if (refTypeConstraint && valueTypeConstraint) + { + throw new Exception($"Type \"{type.Name}\" has invalid generic constraints"); + } + + var viableArguments = new List(); + if (!refTypeConstraint) + { + // Value types are viable + viableArguments.AddRange(new[] + { + typeof(Vector2), + typeof(Point), + typeof(int) + }); + } + if (!valueTypeConstraint) + { + // Reference types are viable + viableArguments.AddRange(new[] + { + typeof(string), + typeof(float[]), + typeof(int[]) + }); + } + + chosenArgs[i] = viableArguments.GetRandomUnsynced(); + } + return type.MakeGenericType(chosenArgs); + } [Fact] public void CheckStructMemberTypes() @@ -29,50 +86,10 @@ public class INetSerializableStructImplementationChecks foreach (var type in types) { - var concreteType = type; - if (type.IsGenericType) - { - // Plug in some known good parameters to evaluate - // a concrete instance of this generic type - - var paramsConstraints = type.GetGenericArguments() - .Select(p => p.GetGenericParameterConstraints()) - .ToImmutableArray(); + var concreteType = type.IsGenericType + ? FillGenericParameters(type) + : type; - var chosenArgs = new Type[paramsConstraints.Length]; - - for (int i = 0; i < paramsConstraints.Length; i++) - { - var constraints = paramsConstraints[i]; - bool refTypeConstraint = constraints.Any(c - => c.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); - bool valueTypeConstraint = constraints.Any(c - => c.GenericParameterAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint)); - if (refTypeConstraint && valueTypeConstraint) - { - throw new Exception($"Type \"{type.Name}\" has invalid generic constraints"); - } - - int rngMin = refTypeConstraint ? 3 : 0; - int rngMax = valueTypeConstraint ? 3 : 6; - - chosenArgs[i] = Rand.Range(rngMin, rngMax) switch - { - 0 => typeof(Vector2), - 1 => typeof(Point), - 2 => typeof(int), - - 3 => typeof(string), - 4 => typeof(float[]), - 5 => typeof(int[]), - - var invalid => throw new Exception($"Broken RNG ranges in test, got {invalid}") - }; - } - - concreteType = type.MakeGenericType(chosenArgs); - } - var members = NetSerializableProperties.GetPropertiesAndFields(concreteType); foreach (var member in members) { diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs index fdd87cc18..85b43d172 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs @@ -217,6 +217,9 @@ namespace TestProject public T NotSerializedFunction() => throw new NotImplementedException(); } + [NetworkSerialize] + private readonly record struct TestRecord(T Value) : INetSerializableStruct; + private struct TupleNullableStruct : INetSerializableStruct { [NetworkSerialize] @@ -248,24 +251,35 @@ namespace TestProject readStruct.IntValue.Should().Be(intValue); } - private static void SerializeDeserialize(T arg) where T : notnull + private static void SerializeDeserializeImpl(T toWrite) where T : INetSerializableStruct { ReadWriteMessage msg = new ReadWriteMessage(); - TestStruct writeStruct = new TestStruct - { - Value = arg - }; - msg.WriteNetSerializableStruct(writeStruct); + msg.WriteNetSerializableStruct(toWrite); msg.BitPosition = 0; - TestStruct readStruct = INetSerializableStruct.Read>(msg); + T read = INetSerializableStruct.Read(msg); - readStruct.Should().BeEquivalentTo(writeStruct, options => options - .ComparingByMembers>() + read.Should().BeEquivalentTo(toWrite, options => options + .ComparingByMembers() .ComparingByMembers(typeof(Option<>))); } + private static void SerializeDeserializeStruct(T arg) where T : notnull + => SerializeDeserializeImpl(new TestStruct + { + Value = arg + }); + + private static void SerializeDeserializeRecord(T arg) where T : notnull + => SerializeDeserializeImpl(new TestRecord(arg)); + + private static void SerializeDeserialize(T arg) where T : notnull + { + SerializeDeserializeStruct(arg); + SerializeDeserializeRecord(arg); + } + private static void SerializeDeserializeNullableTuple(T arg1, U arg2) { ReadWriteMessage msg = new ReadWriteMessage(); diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 6eb0a9776..7a69e3819 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -192,6 +192,22 @@ namespace Steamworks.Ugc } } + /// + /// If installed, the time and date of installation + /// + public DateTime? InstallTime + { + get + { + ulong size = 0; + uint ts = 0; + if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + return null; + + return Epoch.ToDateTime(ts); + } + } + /// /// File size as returned by Steamworks, /// no download/install required diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index d2765ade2..bddd50da5 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -124,7 +124,24 @@ namespace Lidgren.Network m_lastSocketBind = now; if (m_socket == null) - m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + { + try + { + m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + } + catch (SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.AddressFamilyNotSupported) + { + // Fall back to IPv4 if IPv6 is unsupported + m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + } + else + { + throw; + } + } + } if (reBind) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); @@ -132,9 +149,9 @@ namespace Lidgren.Network m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; m_socket.SendBufferSize = m_configuration.SendBufferSize; m_socket.Blocking = false; - m_socket.DualMode = m_configuration.UseDualModeSockets; + if (m_socket.AddressFamily == AddressFamily.InterNetworkV6) { m_socket.DualMode = m_configuration.UseDualModeSockets; } - var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToIPv6(), reBind ? m_listenPort : m_configuration.Port); + var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToFamily(m_socket.AddressFamily), reBind ? m_listenPort : m_configuration.Port); m_socket.Bind(ep); try @@ -413,6 +430,10 @@ namespace Lidgren.Network int bytesReceived = 0; try { + if (m_senderRemote is IPEndPoint ipEndpoint && ipEndpoint.AddressFamily != m_socket.AddressFamily) + { + m_senderRemote = ipEndpoint.MapToFamily(m_socket.AddressFamily); + } bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); } catch (SocketException sx) diff --git a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs index b49585afa..8a255010e 100644 --- a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -136,7 +136,7 @@ namespace Lidgren.Network { connectionReset = false; - target = NetUtility.MapToIPv6(target); + target = target.MapToFamily(m_socket.AddressFamily); IPAddress ba = default(IPAddress); try diff --git a/Libraries/Lidgren.Network/NetPeer.cs b/Libraries/Lidgren.Network/NetPeer.cs index eafef5d7c..538a87996 100644 --- a/Libraries/Lidgren.Network/NetPeer.cs +++ b/Libraries/Lidgren.Network/NetPeer.cs @@ -293,7 +293,7 @@ namespace Lidgren.Network if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint"); - remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint); + remoteEndPoint = remoteEndPoint.MapToFamily(m_socket.AddressFamily); lock (m_connections) { diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index 0c437ce7f..cf5996e98 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -454,16 +454,24 @@ namespace Lidgren.Network return ComputeSHAHash(bytes, 0, bytes.Length); } - /// - /// Maps the IPEndPoint object to an IPv6 address, if it is currently mapped to an IPv4 address. - /// - internal static IPEndPoint MapToIPv6(IPEndPoint endPoint) - { - if (endPoint.AddressFamily == AddressFamily.InterNetwork) - { - return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port); - } - return endPoint; - } + internal static IPAddress MapToFamily(this IPAddress address, AddressFamily family) + { + switch (family) + { + case AddressFamily.InterNetworkV6: + return address.MapToIPv6(); + case AddressFamily.InterNetwork: + return address.MapToIPv4(); + default: + throw new Exception($"Unsupported address family: {family}"); + } + } + + internal static IPEndPoint MapToFamily(this IPEndPoint endpoint, AddressFamily family) + { + return endpoint.Address.AddressFamily == family + ? endpoint + : new IPEndPoint(endpoint.Address.MapToFamily(family), endpoint.Port); + } } } \ No newline at end of file diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index 441214b6e..2d08ff6ec 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -12,15 +12,22 @@ namespace Microsoft.Xna.Framework.Graphics { public interface ISpriteBatch { - public void Draw(Texture2D texture, - Vector2 position, - Rectangle? sourceRectangle, - Color color, - float rotation, - Vector2 origin, - Vector2 scale, - SpriteEffects effects, - float layerDepth); + public void Draw( + Texture2D texture, + Vector2 position, + Rectangle? sourceRectangle, + Color color, + float rotation, + Vector2 origin, + Vector2 scale, + SpriteEffects effects, + float layerDepth); + + public void Draw( + Texture2D texture, + VertexPositionColorTexture[] vertices, + float layerDepth, + int? count = null); } /// From 25fa5a9552f114e7023a8cbccacf24e7d6ab8705 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Tue, 31 Jan 2023 18:01:29 +0200 Subject: [PATCH 02/14] Build 0.21.6.0 --- .../ClientSource/CameraTransition.cs | 14 +- .../ClientSource/Characters/CharacterHUD.cs | 2 + .../Characters/Health/CharacterHealth.cs | 2 + .../Health/HealingCooldownClient.cs | 22 ++ .../ClientSource/Characters/Limb.cs | 181 +++++++++--- .../ContentPackage/ModProject.cs | 8 +- .../ContentPackageManager.cs | 2 +- .../ClientSource/DebugConsole.cs | 24 ++ .../EventActions/CheckObjectiveAction.cs | 8 +- .../Events/EventActions/UIHighlightAction.cs | 30 +- .../ClientSource/GUI/GUIComponent.cs | 5 +- .../ClientSource/GUI/GUIMessageBox.cs | 3 +- .../ClientSource/GUI/GUIStyle.cs | 1 + .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 5 +- .../ClientSource/GUI/UpgradeStore.cs | 2 +- .../BarotraumaClient/ClientSource/GameMain.cs | 14 +- .../ClientSource/GameSession/CrewManager.cs | 10 +- .../ClientSource/Items/CharacterInventory.cs | 18 +- .../ClientSource/Items/Components/Door.cs | 58 ++-- .../Items/Components/LightComponent.cs | 11 +- .../Items/Components/Machines/MiniMap.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 13 +- .../Components/Signal/CustomInterface.cs | 1 + .../ClientSource/Items/Components/Wearable.cs | 5 +- .../ClientSource/Items/Inventory.cs | 9 + .../ClientSource/Items/Item.cs | 10 +- .../ClientSource/Items/ItemPrefab.cs | 10 + .../ClientSource/Map/Levels/WaterRenderer.cs | 11 +- .../ClientSource/Map/Lights/LightManager.cs | 18 +- .../ClientSource/Map/Lights/LightSource.cs | 13 +- .../ClientSource/Map/Map/Map.cs | 43 +-- .../ClientSource/Map/SubmarineInfo.cs | 74 ++++- .../ClientSource/Map/SubmarinePreview.cs | 7 +- .../ClientSource/Networking/BanList.cs | 11 +- .../ClientSource/Networking/ChatMessage.cs | 7 +- .../Networking/Primitives/Peers/ClientPeer.cs | 5 + .../Networking/Voip/VoipClient.cs | 2 +- .../CampaignSetupUI/CampaignSetupUI.cs | 48 +++- .../MultiPlayerCampaignSetupUI.cs | 58 +--- .../SinglePlayerCampaignSetupUI.cs | 110 +++----- .../ClientSource/Screens/CampaignUI.cs | 1 + .../CharacterEditor/CharacterEditorScreen.cs | 28 +- .../ClientSource/Screens/GameScreen.cs | 22 +- .../ClientSource/Screens/MainMenuScreen.cs | 2 +- .../ClientSource/Screens/TestScreen.cs | 3 +- .../Serialization/SerializableEntityEditor.cs | 14 +- .../ClientSource/Sprite/DeformableSprite.cs | 7 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 183 ++++++------ .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 7 +- .../ClientSource/Utils/EffectLoader.cs | 12 + .../ClientSource/Utils/SpriteRecorder.cs | 4 +- .../Content/Effects/wearableclip.xnb | Bin 0 -> 2056 bytes .../Content/Effects/wearableclip_opengl.xnb | Bin 0 -> 1842 bytes .../BarotraumaClient/GlobalSuppressions.cs | 2 + .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/Shaders/Content.mgcb | 5 + .../Shaders/Content_opengl.mgcb | 6 + .../BarotraumaClient/Shaders/wearableclip.fx | 42 +++ .../Shaders/wearableclip_opengl.fx | 42 +++ .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 7 + .../Health/HealingCooldownServer.cs | 51 ++++ .../GameModes/CharacterCampaignData.cs | 11 +- .../GameModes/MultiPlayerCampaign.cs | 16 +- .../ServerSource/Items/Inventory.cs | 19 +- .../ServerSource/Items/Item.cs | 16 +- .../ServerSource/Networking/BanList.cs | 53 ++-- .../ServerSource/Networking/GameServer.cs | 17 +- .../Networking/OrderChatMessage.cs | 1 + .../Primitives/Peers/Server/ServerPeer.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 15 - .../Characters/AI/EnemyAIController.cs | 37 ++- .../Characters/AI/HumanAIController.cs | 50 ++-- .../AI/Objectives/AIObjectiveCombat.cs | 16 +- .../AI/Objectives/AIObjectiveContainItem.cs | 3 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 13 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 10 +- .../AI/Objectives/AIObjectiveGetItem.cs | 10 +- .../AI/Objectives/AIObjectiveGetItems.cs | 4 +- .../AI/Objectives/AIObjectiveManager.cs | 13 +- .../AI/Objectives/AIObjectivePrepare.cs | 7 +- .../AI/Objectives/AIObjectiveRescue.cs | 18 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 25 +- .../Animation/HumanoidAnimController.cs | 2 +- .../SharedSource/Characters/Character.cs | 137 +++++++-- .../Health/Afflictions/AfflictionPrefab.cs | 29 +- .../Characters/Health/CharacterHealth.cs | 2 +- .../SharedSource/Characters/Limb.cs | 17 +- .../Characters/Params/CharacterParams.cs | 5 +- .../ContentPackage/ContentPackage.cs | 9 +- .../SharedSource/DebugConsole.cs | 1 + .../Events/EventActions/UIHighlightAction.cs | 4 +- .../GameSession/AutoItemPlacer.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 14 +- .../SharedSource/GameSession/GameSession.cs | 9 +- .../GameSession/UpgradeManager.cs | 14 +- .../SharedSource/Items/CharacterInventory.cs | 5 +- .../Items/Components/DockingPort.cs | 11 +- .../SharedSource/Items/Components/Door.cs | 10 +- .../Items/Components/Holdable/Holdable.cs | 39 ++- .../Items/Components/Holdable/MeleeWeapon.cs | 6 +- .../Items/Components/Holdable/Pickable.cs | 2 + .../Items/Components/Holdable/Throwable.cs | 4 +- .../Items/Components/ItemComponent.cs | 11 +- .../Items/Components/ItemContainer.cs | 11 +- .../Items/Components/Projectile.cs | 50 ++-- .../SharedSource/Items/Components/Rope.cs | 41 +-- .../Items/Components/Signal/LightComponent.cs | 8 + .../Items/Components/Signal/WifiComponent.cs | 14 +- .../SharedSource/Items/Components/Wearable.cs | 24 +- .../SharedSource/Items/Inventory.cs | 2 + .../SharedSource/Items/Item.cs | 56 ++-- .../SharedSource/Items/ItemPrefab.cs | 15 +- .../SharedSource/Map/Entity.cs | 26 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 6 + .../SharedSource/Map/Levels/LevelData.cs | 18 +- .../SharedSource/Map/Map/Location.cs | 7 +- .../SharedSource/Map/SubmarineInfo.cs | 12 +- .../SharedSource/Networking/BanList.cs | 2 +- .../Networking/INetSerializableStruct.cs | 38 ++- .../Primitives/NetworkPeerStructs.cs | 2 +- .../Serialization/XMLExtensions.cs | 44 ++- .../StatusEffects/StatusEffect.cs | 68 ++--- .../SharedSource/Steam/Workshop.cs | 3 +- .../SharedSource/SteamAchievementManager.cs | 2 + .../SharedSource/Upgrades/Upgrade.cs | 2 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 2 +- .../SharedSource/Utils/CoordinateSpace2D.cs | 26 ++ .../SharedSource/Utils/MathUtils.cs | 3 + .../SharedSource/Utils/SaveUtil.cs | 22 +- .../Utils/SerializableDateTime.cs | 266 ++++++++++++++++++ .../SharedSource/Utils/ToolBox.cs | 43 --- Barotrauma/BarotraumaShared/changelog.txt | 95 ++++++- .../BarotraumaTest/CoordinateSpace2DTests.cs | 55 ++++ ...tSerializableStructImplementationChecks.cs | 4 +- .../SerializableDateTimeTests.cs | 64 +++++ Barotrauma/BarotraumaTest/TestProject.cs | 4 +- .../Classes/AuthTicket.cs | 31 +- .../SteamMatchmakingResponses.cs | 143 ++++++---- .../Utility/SourceServerQuery.cs | 235 ++++------------ 145 files changed, 2317 insertions(+), 1145 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs create mode 100644 Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb create mode 100644 Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb create mode 100644 Barotrauma/BarotraumaClient/Shaders/wearableclip.fx create mode 100644 Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs create mode 100644 Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs create mode 100644 Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index ed7ff67d7..b1392f9cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -154,9 +154,12 @@ namespace Barotrauma } if (LosFadeIn && clampedTimer / PanDuration > 0.8f) { - GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f; + if (!GameMain.DevMode) + { + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f; + } Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); - GameMain.LightManager.LosEnabled = true; } #endif timer += CoroutineManager.DeltaTime; @@ -170,8 +173,11 @@ namespace Barotrauma #if CLIENT GUI.ScreenOverlayColor = Color.TransparentBlack; - GameMain.LightManager.LosEnabled = true; - GameMain.LightManager.LosAlpha = 1f; + if (!GameMain.DevMode) + { + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + } #endif if (prevControlled != null && !prevControlled.Removed) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index d0b25b398..80b153e28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -724,6 +724,8 @@ namespace Barotrauma } bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; + Color color = bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("poison") > 0 || bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("paralysis") > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; + bossHealthBar.TopHealthBar.Color = bossHealthBar.SideHealthBar.Color = color; if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d9801606c..6fe86291d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2071,6 +2071,8 @@ namespace Barotrauma foreach (var periodicEffect in newPeriodicEffects) { if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } + if (existingAffliction.Strength < periodicEffect.effect.MinStrength) { continue; } + if (periodicEffect.effect.MaxStrength > 0 && existingAffliction.Strength > periodicEffect.effect.MaxStrength) { continue; } //timer has wrapped around, apply the effect if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs new file mode 100644 index 000000000..bcc7d4083 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs @@ -0,0 +1,22 @@ +#nullable enable + +using System; + +namespace Barotrauma +{ + internal static class HealingCooldown + { + public static float NormalizedCooldown => MathF.Min((float) (DateTimeOffset.UtcNow - OnCooldownUntil).TotalSeconds / CooldownDuration, 0f); + public static bool IsOnCooldown => DateTimeOffset.UtcNow < OnCooldownUntil; + + private static DateTimeOffset OnCooldownUntil = DateTimeOffset.MinValue; + private const float CooldownDuration = 0.5f; + + public static readonly Identifier MedicalItemTag = new Identifier("medical"); + + public static void PutOnCooldown() + { + OnCooldownUntil = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 4a8c36df7..5fe12e73e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Barotrauma.IO; +using Barotrauma.Utils; using System.Linq; using System.Xml.Linq; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; @@ -260,9 +261,7 @@ namespace Barotrauma { if (enableHuskSprite) { - List otherWearablesWithHusk = new List() { HuskSprite }; - otherWearablesWithHusk.AddRange(OtherWearables); - OtherWearables = otherWearablesWithHusk; + OtherWearables.Insert(0, HuskSprite); UpdateWearableTypesToHide(); } else @@ -546,7 +545,7 @@ namespace Barotrauma { foreach (var affliction in result.Afflictions) { - if (affliction is AfflictionBleeding) + if (affliction is AfflictionBleeding bleeding && bleeding.Prefab.DamageParticles) { bleedingDamage += affliction.GetVitalityDecrease(null); } @@ -555,7 +554,7 @@ namespace Barotrauma float damage = 0; foreach (var affliction in result.Afflictions) { - if (affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == "damage") { damage += affliction.GetVitalityDecrease(null); } @@ -732,7 +731,7 @@ namespace Barotrauma bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || - wearingItems.Any(w => w != null && w.HideLimb); + WearingItems.Any(w => w.HideLimb); bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); @@ -828,7 +827,7 @@ namespace Barotrauma LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } float step = depthStep; - WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); + WearableSprite onlyDrawable = WearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) { spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; @@ -965,31 +964,28 @@ namespace Barotrauma public void UpdateWearableTypesToHide() { + alphaClipEffectParams?.Clear(); + wearableTypeHidingSprites.Clear(); - if (WearingItems != null && WearingItems.Count > 0) + + void addWearablesFrom(IReadOnlyList wearableSprites) { + if (wearableSprites.Count <= 0) { return; } + wearableTypeHidingSprites.AddRange( - WearingItems.FindAll(w => w.HideWearablesOfType != null && w.HideWearablesOfType.Count > 0)); - } - if (OtherWearables != null && OtherWearables.Count > 0) - { - wearableTypeHidingSprites.AddRange( - OtherWearables.FindAll(w => w.HideWearablesOfType != null && w.HideWearablesOfType.Count > 0)); + wearableSprites.Where(w => w.HideWearablesOfType.Count > 0)); } + addWearablesFrom(WearingItems); + addWearablesFrom(OtherWearables); + wearableTypesToHide.Clear(); - if (wearableTypeHidingSprites.Count > 0) + + if (wearableTypeHidingSprites.Count <= 0) { return; } + + foreach (WearableSprite sprite in wearableTypeHidingSprites) { - foreach (WearableSprite sprite in wearableTypeHidingSprites) - { - foreach (WearableType type in sprite.HideWearablesOfType) - { - if (!wearableTypesToHide.Contains(type)) - { - wearableTypesToHide.Add(type); - } - } - } + wearableTypesToHide.UnionWith(sprite.HideWearablesOfType); } } @@ -1071,7 +1067,13 @@ namespace Barotrauma } } - private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) + private ( + Color FinalColor, + Vector2 Origin, + float Rotation, + float Scale, + float Depth) + CalculateDrawParameters(WearableSprite wearable, float depthStep, Color color, float alpha) { var sprite = ActiveSprite; if (wearable.InheritSourceRect) @@ -1163,27 +1165,118 @@ namespace Barotrauma float finalAlpha = alpha * wearableColor.A; Color finalColor = color.Multiply(wearableColor); finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha); - wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); + + return (finalColor, origin, rotation, scale, depth); } - private WearableSprite GetWearableSprite(WearableType type)//, bool random = false) + private static Effect alphaClipEffect; + private Dictionary> alphaClipEffectParams; + private void ApplyAlphaClip(SpriteBatch spriteBatch, WearableSprite wearable, WearableSprite alphaClipper, SpriteEffects spriteEffect) + { + SpriteRecorder.Command makeCommand(WearableSprite w) + { + var (_, origin, rotation, scale, _) + = CalculateDrawParameters(w, 0f, Color.White, 0f); + + var command = SpriteRecorder.Command.FromTransform( + texture: w.Sprite.Texture, + pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), + srcRect: w.Sprite.SourceRect, + color: Color.White, + rotation: rotation, + origin: origin, + scale: new Vector2(scale, scale), + effects: spriteEffect, + depth: 0f, + index: 0); + + return command; + } + + void spacesFromCommand(WearableSprite w, SpriteRecorder.Command command, out CoordinateSpace2D textureSpace, out CoordinateSpace2D worldSpace) + { + var (topLeft, bottomLeft, topRight) = spriteEffect switch + { + SpriteEffects.None + => (command.VertexTL, command.VertexBL, command.VertexTR), + SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically + => (command.VertexBR, command.VertexTR, command.VertexBL), + SpriteEffects.FlipHorizontally + => (command.VertexTR, command.VertexBR, command.VertexTL), + SpriteEffects.FlipVertically + => (command.VertexBL, command.VertexTL, command.VertexBR) + }; + + textureSpace = new CoordinateSpace2D + { + Origin = topLeft.TextureCoordinate, + I = topRight.TextureCoordinate - topLeft.TextureCoordinate, + J = bottomLeft.TextureCoordinate - topLeft.TextureCoordinate + }; + + worldSpace = new CoordinateSpace2D + { + Origin = topLeft.Position.DiscardZ(), + I = topRight.Position.DiscardZ() - topLeft.Position.DiscardZ(), + J = bottomLeft.Position.DiscardZ() - topLeft.Position.DiscardZ() + }; + } + + var wearableCommand = makeCommand(wearable); + var clipperCommand = makeCommand(alphaClipper); + + spacesFromCommand(wearable, wearableCommand, out var wearableTextureSpace, out var wearableWorldSpace); + spacesFromCommand(alphaClipper, clipperCommand, out var clipperTextureSpace, out var clipperWorldSpace); + + var wearableUvToClipperUv = + wearableTextureSpace.CanonicalToLocal + * wearableWorldSpace.LocalToCanonical + * clipperWorldSpace.CanonicalToLocal + * clipperTextureSpace.LocalToCanonical; + + alphaClipEffect ??= EffectLoader.Load("Effects/wearableclip"); + alphaClipEffectParams ??= new Dictionary>(); + if (!alphaClipEffectParams.ContainsKey(wearable)) { alphaClipEffectParams.Add(wearable, new Dictionary()); } + + var paramsToPass = new SpriteBatch.EffectWithParams + { + Effect = alphaClipEffect, + Params = alphaClipEffectParams[wearable] + }; + + paramsToPass.Params["wearableUvToClipperUv"] = wearableUvToClipperUv; + paramsToPass.Params["clipperTexelSize"] = 2f / alphaClipper.Sprite.Texture.Width; + paramsToPass.Params["aCutoff"] = 2f / 255f; + paramsToPass.Params["xTexture"] = wearable.Sprite.Texture; + paramsToPass.Params["xStencil"] = alphaClipper.Sprite.Texture; + spriteBatch.SwapEffect(paramsToPass); + } + + private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) + { + var (finalColor, origin, rotation, scale, depth) + = CalculateDrawParameters(wearable, depthStep, color, alpha); + + var prevEffect = spriteBatch.GetCurrentEffect(); + var alphaClipper = WearingItems.Find(w => w.AlphaClipOtherWearables); + bool shouldApplyAlphaClip = alphaClipper != null && wearable != alphaClipper; + if (shouldApplyAlphaClip) + { + ApplyAlphaClip(spriteBatch, wearable, alphaClipper, spriteEffect); + } + wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); + if (shouldApplyAlphaClip) + { + spriteBatch.SwapEffect(effect: prevEffect); + } + } + + private WearableSprite GetWearableSprite(WearableType type) { var info = character.Info; if (info == null) { return null; } - ContentXElement element; - /*if (random) - { - element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet)?.GetRandom(Rand.RandSync.ClientOnly); - } - else - {*/ - element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); - //} - if (element != null) - { - return new WearableSprite(element.GetChildElement("sprite"), type); - } - return null; + ContentXElement element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); + return element != null ? new WearableSprite(element.GetChildElement("sprite"), type) : null; } partial void RemoveProjSpecific() @@ -1206,8 +1299,8 @@ namespace Barotrauma LightSource?.Remove(); LightSource = null; - OtherWearables?.ForEach(w => w.Sprite.Remove()); - OtherWearables = null; + OtherWearables.ForEach(w => w.Sprite.Remove()); + OtherWearables.Clear(); HuskSprite?.Sprite.Remove(); HuskSprite = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 85877787b..76afc54f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -92,7 +92,7 @@ namespace Barotrauma public Option UgcId = Option.None(); - public Option InstallTime = Option.None(); + public Option InstallTime = Option.None(); public bool HasFile(File file) => Files.Any(f => @@ -120,7 +120,7 @@ namespace Barotrauma public void DiscardHashAndInstallTime() { ExpectedHash = null; - InstallTime = Option.None(); + InstallTime = Option.None(); } public static string IncrementModVersion(string modVersion) @@ -159,8 +159,8 @@ namespace Barotrauma addRootAttribute("gameversion", GameMain.Version); if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } - if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(installTime)); } - + if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", installTime); } + files.ForEach(f => rootElement.Add(f.ToXElement())); doc.Add(rootElement); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 92ce38770..95e5c377a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -49,7 +49,7 @@ namespace Barotrauma && ugcId is SteamWorkshopId workshopId && item.Id == workshopId.Value && p.InstallTime.TryUnwrap(out var installTime) - && item.LatestUpdateTime <= installTime)) + && item.LatestUpdateTime <= installTime.ToUtcValue())) .ToArray(); if (!needInstalling.Any()) { return Enumerable.Empty(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 2833cf1d1..19d98e5e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1128,6 +1128,28 @@ namespace Barotrauma }); AssignRelayToServer("debugdraw", false); + AssignOnExecute("devmode", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.DevMode; + } + GameMain.DevMode = state; + if (GameMain.DevMode) + { + GameMain.LightManager.LightingEnabled = false; + GameMain.LightManager.LosEnabled = false; + } + else + { + GameMain.LightManager.LightingEnabled = true; + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + } + NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White); + }); + AssignRelayToServer("devmode", false); + AssignOnExecute("debugdrawlocalization", (string[] args) => { if (args.None() || !bool.TryParse(args[0], out bool state)) @@ -1229,12 +1251,14 @@ namespace Barotrauma HumanAIController.debugai = !HumanAIController.debugai; if (HumanAIController.debugai) { + GameMain.DevMode = true; GameMain.DebugDraw = true; GameMain.LightManager.LightingEnabled = false; GameMain.LightManager.LosEnabled = false; } else { + GameMain.DevMode = false; GameMain.DebugDraw = false; GameMain.LightManager.LightingEnabled = true; GameMain.LightManager.LosEnabled = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs index ce251c05a..55f82a1d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -8,7 +8,8 @@ partial class CheckObjectiveAction : BinaryOptionAction public enum CheckType { Added, - Completed + Completed, + Incomplete } [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] @@ -30,8 +31,13 @@ partial class CheckObjectiveAction : BinaryOptionAction { CheckType.Added => true, CheckType.Completed => segment.IsCompleted, + CheckType.Incomplete => !segment.IsCompleted, _ => false }; } + else if (Type == CheckType.Incomplete) + { + success = true; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs index 809bdb393..5e4415bd6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma; @@ -13,11 +14,22 @@ partial class UIHighlightAction : EventAction bool useCircularFlash = false; if (Id != ElementId.None) { - FindAndFlashComponents(c => Equals(Id, c.UserData)); + var predicate = (GUIComponent c) => c is not null && Equals(Id, c.UserData); + if (!FindAndFlashAddedComponents(predicate)) + { + if (predicate(GUIMessageBox.VisibleBox)) + { + Flash(GUIMessageBox.VisibleBox); + } + else + { + FindAndFlashMessageBoxComponents(predicate); + } + } } else if (!EntityIdentifier.IsEmpty) { - FindAndFlashComponents(c => + FindAndFlashAddedComponents(c => c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier); } else if (!OrderIdentifier.IsEmpty) @@ -26,26 +38,26 @@ partial class UIHighlightAction : EventAction bool foundMinimapNode = false; if (!OrderTargetTag.IsEmpty) { - foundMinimapNode = FindAndFlashComponents(c => + foundMinimapNode = FindAndFlashAddedComponents(c => c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); } if (!foundMinimapNode) { - FindAndFlashComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption, + FindAndFlashAddedComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption, c => c.UserData is Order order && order.Identifier == OrderIdentifier, c => Equals(OrderCategory, c.UserData)); } } - bool FindAndFlashComponents(params Func[] predicates) + bool FindAndFlashComponents(IEnumerable components, params Func[] predicates) { foreach (var predicate in predicates) { if (HighlightMultiple) { bool found = false; - foreach (var component in GUI.GetAdditions()) + foreach (var component in components) { if (predicate(component)) { @@ -55,7 +67,7 @@ partial class UIHighlightAction : EventAction }; return found; } - else if (GUI.GetAdditions().FirstOrDefault(predicate) is GUIComponent component) + else if (components.FirstOrDefault(predicate) is GUIComponent component) { Flash(component); return true; @@ -64,6 +76,10 @@ partial class UIHighlightAction : EventAction return false; } + bool FindAndFlashAddedComponents(params Func[] predicates) => FindAndFlashComponents(GUI.GetAdditions(), predicates); + + bool FindAndFlashMessageBoxComponents(params Func[] predicates) => FindAndFlashComponents(GUIMessageBox.VisibleBox?.GetAllChildren() ?? Enumerable.Empty(), predicates); + void Flash(GUIComponent component) { if (component.FlashTimer <= 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 96a646db8..b6460f83f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1143,14 +1143,13 @@ namespace Barotrauma bool wrap = element.GetAttributeBool("wrap", true); Alignment alignment = element.GetAttributeEnum("alignment", text.Contains('\n') ? Alignment.Left : Alignment.Center); - GUIFont font; - if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out font)) + if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out GUIFont font)) { font = GUIStyle.Font; } var textBlock = new GUITextBlock(RectTransform.Load(element, parent), - text, color, font, alignment, wrap: wrap, style: style) + RichString.Rich(text), color, font, alignment, wrap: wrap, style: style) { TextScale = scale }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 81ad4ac35..fc1ade91e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -236,7 +236,8 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center), style: "UIToggleButton") { - OnClicked = Close + OnClicked = Close, + UserData = UIHighlightAction.ElementId.MessageBoxCloseButton } }; InputType? closeInput = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 07e976ada..e3ad34ad7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -140,6 +140,7 @@ namespace Barotrauma 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 static Point ItemFrameMargin { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 21481ef4e..ed1740549 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -615,7 +615,7 @@ namespace Barotrauma listBackground.SetCrop(true); GUIFont font = GUIStyle.Font; - info.CreateSpecsWindow(specsFrame, font); + info.CreateSpecsWindow(specsFrame, font, includeCrushDepth: true); descriptionTextBlock.Text = info.Description; descriptionTextBlock.CalculateHeightFromText(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index ee9de1b18..75219550f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1779,7 +1779,10 @@ namespace Barotrauma { CurrentSelectMode = GUIListBox.SelectMode.None }; - sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); + sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, + includeTitle: false, + includeClass: false, + includeDescription: true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3399c5399..3a2e531c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1171,7 +1171,7 @@ namespace Barotrauma materialCostList.Visible = false; materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; - var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice) + var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Right) { UserData = UpgradeStoreUserData.PriceLabel, //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 2654387fe..a730310d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -23,9 +23,13 @@ namespace Barotrauma { class GameMain : Game { - public static bool ShowFPS = false; - public static bool ShowPerf = false; + public static bool ShowFPS; + public static bool ShowPerf; public static bool DebugDraw; + /// + /// Doesn't automatically enable los or bot AI or do anything like that. Probably not fully implemented. + /// + public static bool DevMode; public static bool IsSingleplayer => NetworkMember == null; public static bool IsMultiplayer => NetworkMember != null; @@ -398,7 +402,7 @@ namespace Barotrauma TextureLoader.Init(GraphicsDevice); //do this here because we need it for the loading screen - WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content); + WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice); Quad.Init(GraphicsDevice); @@ -512,10 +516,10 @@ namespace Barotrauma TitleScreen.LoadState = 75.0f; yield return CoroutineStatus.Running; - GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice, Content); + GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice); ParticleManager = new ParticleManager(GameScreen.Cam); - LightManager = new Lights.LightManager(base.GraphicsDevice, Content); + LightManager = new Lights.LightManager(base.GraphicsDevice); TitleScreen.LoadState = 80.0f; yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 5820ca4cd..7a09abd5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -788,7 +788,6 @@ namespace Barotrauma { return; } - if (ws != null) { hull = Hull.FindHull(ws.WorldPosition); @@ -802,7 +801,6 @@ namespace Barotrauma hull = Hull.FindHull(se.WorldPosition); } } - if (IsSinglePlayer) { order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); @@ -817,13 +815,13 @@ namespace Barotrauma { //can't issue an order if no characters are available if (character == null) { return; } - var orderGiver = order?.OrderGiver; if (IsSinglePlayer) { - character.SetOrder(order, isNewOrder, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); - orderGiver?.Speak(message); + bool isGivingOrderToSelf = orderGiver == character; + character.SetOrder(order, isNewOrder, speak: !isGivingOrderToSelf); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, isGivingOrderToSelf, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); + orderGiver?.Speak(message); } else if (orderGiver != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 11530061e..18139a698 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -965,7 +965,7 @@ namespace Barotrauma break; case QuickUseAction.PutToEquippedItem: //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.OrderBy(it => it.GetComponent()?.GetContainedIndicatorState() ?? 0.0f)) + foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem))) { if (heldItem.OwnInventory == null) { continue; } //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items @@ -986,6 +986,22 @@ namespace Barotrauma } } break; + + static float GetContainPriority(Item item, Item containerItem) + { + var container = containerItem.GetComponent(); + if (container == null) { return 0.0f; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + var containedItems = container.Inventory.GetItemsAt(i); + if (containedItems.Any() && container.Inventory.CanBePutInSlot(item, i)) + { + //if there's a stack in the contained item that we can add the item to, prefer that + return 10.0f; + } + } + return -container.GetContainedIndicatorState(); + } } if (success) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index aa7baf8be..f5faabb79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -216,16 +216,19 @@ namespace Barotrauma.Items.Components if (brokenSprite == null || !IsBroken) { - spriteBatch.Draw(doorSprite.Texture, pos, - getSourceRect(doorSprite, openState, IsHorizontal), - color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); + if (doorSprite?.Texture != null) + { + spriteBatch.Draw(doorSprite.Texture, pos, + getSourceRect(doorSprite, openState, IsHorizontal), + color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); + } } float maxCondition = item.Repairables.Any() ? item.Repairables.Min(r => r.RepairThreshold) / 100.0f * item.MaxCondition : item.MaxCondition; float healthRatio = item.Health / maxCondition; - if (brokenSprite != null && healthRatio < 1.0f) + if (brokenSprite?.Texture != null && healthRatio < 1.0f) { Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - healthRatio) : Vector2.One; if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; } @@ -285,34 +288,45 @@ namespace Barotrauma.Items.Components //sent by the server, or reverting it back to its old state if no msg from server was received PredictedState = open; resetPredictionTimer = CorrectionDelay; - if (stateChanged) PlaySound(forcedOpen ? ActionType.OnPicked : ActionType.OnUse); + if (stateChanged && !IsBroken) + { + PlayInteractionSound(); + } } else { isOpen = open; if (!isNetworkMessage || open != PredictedState) { - StopPicking(null); - ActionType actionType = ActionType.OnUse; - if (forcedOpen) + StopPicking(null); + if (!IsBroken) { - actionType = ActionType.OnPicked; + PlayInteractionSound(); } - else - { - if (open && HasSoundsOfType[(int)ActionType.OnOpen]) - { - actionType = ActionType.OnOpen; - } - else if (!open && HasSoundsOfType[(int)ActionType.OnClose]) - { - actionType = ActionType.OnClose; - } - } - PlaySound(actionType); if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } - } + } + + void PlayInteractionSound() + { + ActionType actionType = ActionType.OnUse; + if (forcedOpen) + { + actionType = ActionType.OnPicked; + } + else + { + if (open && HasSoundsOfType[(int)ActionType.OnOpen]) + { + actionType = ActionType.OnOpen; + } + else if (!open && HasSoundsOfType[(int)ActionType.OnClose]) + { + actionType = ActionType.OnClose; + } + } + PlaySound(actionType); + } } public override void ClientEventRead(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index b8d40669d..2c764937c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -78,14 +78,21 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) + if (Light?.LightSprite == null) { return; } + if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) { Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; } Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition; - Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + + Color color = lightColor; + if (Light.OverrideLightSpriteAlpha.HasValue) + { + color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); + } + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 85ac7fd01..f901ec9ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -413,7 +413,7 @@ namespace Barotrauma.Items.Components var wire = it.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } - if (it.Container?.GetComponent() is { DrawInventory: false }) { return false; } + if (it.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } if (it.HasTag("traitormissionitem")) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 9705b407b..56194db32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -72,7 +72,9 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; public bool TriggerInfographic { get; set; } - + + public bool IsInfographicVisible => infographic != null && infographic.Visible; + partial void InitProjSpecific(ContentXElement element) { CreateGUI(); @@ -108,6 +110,9 @@ namespace Barotrauma.Items.Components { AbsoluteOffset = GUIStyle.ItemFrameOffset }, isHorizontal: true) { + CanBeFocused = true, + HoverCursor = CursorState.Default, + AlwaysOverrideCursor = true, RelativeSpacing = 0.012f, Stretch = true }; @@ -675,7 +680,7 @@ namespace Barotrauma.Items.Components } } - if (TriggerInfographic) + if (GuiFrame is not null && GuiFrame.Visible && TriggerInfographic) { CreateInfrographic(); TriggerInfographic = false; @@ -851,8 +856,9 @@ namespace Barotrauma.Items.Components { AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale) }; - new GUIButton(closeButtonRt, TextManager.Get("close")) + new GUIButton(closeButtonRt, TextManager.Get("closeinfographic")) { + UserData = UIHighlightAction.ElementId.CloseButton, OnClicked = (_, _) => { CloseInfographic(Character.Controlled); @@ -871,6 +877,7 @@ namespace Barotrauma.Items.Components string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved"; return new GUIImage(rt, style) { + CanBeFocused = false, Rotation = MathHelper.ToRadians(rotationDegrees), SpriteEffects = spriteEffects }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 17b151087..29b3adae2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -329,6 +329,7 @@ namespace Barotrauma.Items.Components partial void UpdateSignalsProjSpecific() { + if (signals == null) { return; } for (int i = 0; i < signals.Length && i < uiElements.Count; i++) { if (uiElements[i] is GUITextBox tb) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 58093440a..6693d594b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { - int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); + int roundedValue = (int)Math.Round((1 - Math.Min(damageModifier.DamageMultiplier, damageModifier.ProbabilityMultiplier)) * 100); if (roundedValue == 0) { return; } string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green); @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components TextManager.Get($"afflictiontype.{afflictionIdentifier}").Fallback(afflictionIdentifier.Value); if (!description.IsNullOrWhiteSpace()) { description += '\n'; } - description += $" ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; + description += $" ‖color:{colorStr}‖{roundedValue:-0;+#}%‖color:end‖ {afflictionName}"; } public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) @@ -36,7 +36,6 @@ namespace Barotrauma.Items.Components { continue; } - foreach (Identifier afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) { GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 8728ef139..5b4d60065 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1713,6 +1713,15 @@ namespace Barotrauma GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } } + + if (HealingCooldown.IsOnCooldown && item.HasTag(HealingCooldown.MedicalItemTag)) + { + RectangleF cdRect = rect; + // shrink the rect from top to bottom depending on HealingCooldown.NormalizedCooldown + cdRect.Height *= HealingCooldown.NormalizedCooldown; + cdRect.Y += rect.Height; + GUI.DrawFilledRectangle(spriteBatch, cdRect, Color.White * 0.5f); + } } if (inventory != null && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 9bd922451..684cb0d5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -203,7 +203,7 @@ namespace Barotrauma } } - partial void InitProjSpecific() + public void InitSpriteStates() { Prefab.Sprite?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded(); @@ -211,7 +211,6 @@ namespace Barotrauma { brokenSprite.Sprite.EnsureLazyLoaded(); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); @@ -221,6 +220,11 @@ namespace Barotrauma UpdateSpriteStates(0.0f); } + partial void InitProjSpecific() + { + InitSpriteStates(); + } + private Rectangle? cachedVisibleExtents; public void ResetCachedVisibleSize() @@ -1409,7 +1413,7 @@ namespace Barotrauma if (targetComponent == null) { - ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, true, worldPosition: worldPosition); + ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, isNetworkEvent: true, worldPosition: worldPosition); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index ee6bae0c0..17cd3e848 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -236,6 +236,16 @@ namespace Barotrauma DecorativeSprites = decorativeSprites.ToImmutableArray(); ContainedSprites = containedSprites.ToImmutableArray(); DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + +#if CLIENT + foreach (Item item in Item.ItemList) + { + if (item.Prefab == this) + { + item.InitSpriteStates(); + } + } +#endif } public bool CanCharacterBuy() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index a053bcc73..9ce97aa94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -65,15 +64,9 @@ namespace Barotrauma public Texture2D WaterTexture { get; } - public WaterRenderer(GraphicsDevice graphicsDevice, ContentManager content) + public WaterRenderer(GraphicsDevice graphicsDevice) { -#if WINDOWS - WaterEffect = content.Load("Effects/watershader"); -#endif -#if LINUX || OSX - - WaterEffect = content.Load("Effects/watershader_opengl"); -#endif + WaterEffect = EffectLoader.Load("Effects/watershader"); WaterTexture = TextureLoader.FromFile("Content/Effects/waterbump.png"); WaterEffect.Parameters["xWaterBumpMap"].SetValue(WaterTexture); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e3aec5d91..d8314b1b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Content; using System.Collections.Generic; using System.Linq; using System; @@ -73,12 +72,14 @@ namespace Barotrauma.Lights private int recalculationCount; + private float time; + public IEnumerable Lights { get { return lights; } } - public LightManager(GraphicsDevice graphics, ContentManager content) + public LightManager(GraphicsDevice graphics) { lights = new List(100); @@ -96,13 +97,8 @@ namespace Barotrauma.Lights { CreateRenderTargets(graphics); -#if WINDOWS - LosEffect = content.Load("Effects/losshader"); - SolidColorEffect = content.Load("Effects/solidcolor"); -#else - LosEffect = content.Load("Effects/losshader_opengl"); - SolidColorEffect = content.Load("Effects/solidcolor_opengl"); -#endif + LosEffect = EffectLoader.Load("Effects/losshader"); + SolidColorEffect = EffectLoader.Load("Effects/solidcolor"); if (lightEffect == null) { @@ -171,10 +167,12 @@ namespace Barotrauma.Lights public void Update(float deltaTime) { + //wrap around if the timer gets very large, otherwise we'd start running into floating point accuracy issues + time = (time + deltaTime) % 100000.0f; foreach (LightSource light in activeLights) { if (!light.Enabled) { continue; } - light.Update(deltaTime); + light.Update(time); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 6fe45ed52..7f981e74f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -200,8 +200,6 @@ namespace Barotrauma.Lights private static Texture2D lightTexture; - private float blinkTimer, flickerState, pulseState; - private VertexPositionColorTexture[] vertices; private short[] indices; @@ -486,12 +484,12 @@ namespace Barotrauma.Lights if (addLight) { GameMain.LightManager.AddLight(this); } } - public void Update(float deltaTime) + public void Update(float time) { float brightness = 1.0f; if (lightSourceParams.BlinkFrequency > 0.0f) { - blinkTimer = (blinkTimer + deltaTime * lightSourceParams.BlinkFrequency) % 1.0f; + float blinkTimer = (time * lightSourceParams.BlinkFrequency) % 1.0f; if (blinkTimer > 0.5f) { CurrentBrightness = 0.0f; @@ -500,14 +498,13 @@ namespace Barotrauma.Lights } if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f) { - pulseState = (pulseState + deltaTime * lightSourceParams.PulseFrequency) % 1.0f; + float pulseState = (time * lightSourceParams.PulseFrequency) % 1.0f; //oscillate between 0-1 brightness *= 1.0f - (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount; } - if (lightSourceParams.Flicker > 0.0f) + if (lightSourceParams.Flicker > 0.0f && lightSourceParams.FlickerSpeed > 0.0f) { - flickerState += deltaTime * lightSourceParams.FlickerSpeed; - flickerState %= 255; + float flickerState = (time * lightSourceParams.FlickerSpeed) % 255; brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; } CurrentBrightness = brightness; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 8339eb987..bdc94b8de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -68,7 +68,7 @@ namespace Barotrauma private (Rectangle targetArea, RichString tip)? tooltip; - private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; + private SubmarineInfo.PendingSubInfo pendingSubInfo; private RichString beaconStationActiveText, beaconStationInactiveText; @@ -936,39 +936,8 @@ namespace Barotrauma if (connection.LevelData.HasHuntingGrounds) { iconCount++; } if (connection.Locked) { iconCount++; } string tooltip = null; - float subCrushDepth = Level.DefaultRealWorldCrushDepth; - var currentOrPendingSub = SubmarineSelection.CurrentOrPendingSubmarine(); - if (Submarine.MainSub != null && Submarine.MainSub.Info == currentOrPendingSub) - { - subCrushDepth = Submarine.MainSub.RealWorldCrushDepth; - } - else if (currentOrPendingSub != null) - { - if (pendingSubInfo.pendingSub != currentOrPendingSub) - { - // Store the real world crush depth for the pending sub so that we don't have to calculate it again every time - pendingSubInfo = (currentOrPendingSub, currentOrPendingSub.GetRealWorldCrushDepth()); - } - subCrushDepth = pendingSubInfo.realWorldCrushDepth; - } - if (GameMain.GameSession?.Campaign?.UpgradeManager != null) - { - var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth".ToIdentifier()); - if (hullUpgradePrefab != null) - { - int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); - int currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); - if (pendingLevel > currentLevel) - { - string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); - if (!string.IsNullOrEmpty(updateValueStr)) - { - subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel - currentLevel, updateValueStr); - } - } - } - } + float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo); string crushDepthWarningIconStyle = null; if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) { @@ -1125,6 +1094,14 @@ namespace Barotrauma } } + /// + /// Resets and forces crush depth to be calculated again for icon displaying purposes + /// + public void ResetPendingSub() + { + pendingSubInfo = new SubmarineInfo.PendingSubInfo(); + } + partial void RemoveProjSpecific() { noiseOverlay?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index a08893601..f15259990 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -89,7 +89,11 @@ namespace Barotrauma CreateSpecsWindow(descriptionBox, font, includeDescription: true); } - public void CreateSpecsWindow(GUIListBox parent, GUIFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) + public void CreateSpecsWindow(GUIListBox parent, GUIFont font, + bool includeTitle = true, + bool includeClass = true, + bool includeDescription = false, + bool includeCrushDepth = false) { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; @@ -155,6 +159,22 @@ namespace Barotrauma { CanBeFocused = false }; cargoCapacityText.RectTransform.MinSize = new Point(0, cargoCapacityText.Children.First().Rect.Height); + if (includeCrushDepth) + { + var crushDepthText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("CrushDepth"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crushDepthText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.GetWithVariable("meterformat", "[meters]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetSubCrushDepth())), + textAlignment: Alignment.TopLeft, font: font, wrap: true) + { + CanBeFocused = false + }; + crushDepthText.RectTransform.MinSize = new Point(0, crushDepthText.Children.First().Rect.Height); + } + if (RecommendedCrewSizeMax > 0) { var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), @@ -227,5 +247,57 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren().Where(c => c != submarineNameText && c != descBlock)); parent.ForceLayoutRecalculation(); } + + public readonly record struct PendingSubInfo(SubmarineInfo PendingSub = null, bool StructuresDefineRealWorldCrushDepth = false, float RealWorldCrushDepth = Level.DefaultRealWorldCrushDepth); + + private float GetSubCrushDepth() + { + var pendingSubInfo = new PendingSubInfo(); + return GetSubCrushDepth(this, ref pendingSubInfo); + } + + public static float GetSubCrushDepth(SubmarineInfo subInfo, ref PendingSubInfo pendingSubInfo) + { + float subCrushDepth = Level.DefaultRealWorldCrushDepth; + if (Submarine.MainSub != null && Submarine.MainSub.Info == subInfo) + { + subCrushDepth = Submarine.MainSub.RealWorldCrushDepth; + } + else if (subInfo != null) + { + if (pendingSubInfo.PendingSub != subInfo) + { + // Store the real world crush depth for the pending sub so that we don't have to calculate it again every time + pendingSubInfo = new PendingSubInfo(subInfo, subInfo.IsCrushDepthDefinedInStructures(out float realWorldCrushDepth), realWorldCrushDepth); + } + subCrushDepth = pendingSubInfo.RealWorldCrushDepth; + } + if (GameMain.GameSession?.Campaign?.UpgradeManager != null && UpgradePrefab.Find("increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab) + { + int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), info: subInfo); + // If there is a sub switch pending, unless its structures have crush depth defined in their elements, + // calculate the value based on the default crush depth and pending upgrade level + int currentLevel = 0; + if (pendingSubInfo.PendingSub is null || pendingSubInfo.StructuresDefineRealWorldCrushDepth) + { + currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevelForSub(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First(), subInfo); + } + if (pendingLevel > currentLevel) + { + string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); + if (!string.IsNullOrEmpty(updateValueStr)) + { + if (currentLevel > 0) + { + // If the current level is greater than 0, reset the crush depth value back to the base value before calculating the upgrade + int upgradePercentage = UpgradePrefab.ParsePercentage(updateValueStr, Identifier.Empty, suppressWarnings: true); + subCrushDepth /= (1f + (upgradePercentage / 100f * currentLevel)); + } + subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel, updateValueStr); + } + } + } + return subCrushDepth; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index a4087003a..b3f830140 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -45,7 +45,7 @@ namespace Barotrauma } } - private struct Door + private readonly struct Door { public readonly Rectangle Rect; @@ -153,7 +153,9 @@ namespace Barotrauma ScrollBarVisible = false, Spacing = GUI.IntScale(5) }; - subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); + subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, + includeTitle: false, + includeDescription: true); int width = specsContainer.Rect.Width; void recalculateSpecsContainerHeight() { @@ -191,6 +193,7 @@ namespace Barotrauma TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ => { + if (isDisposed) { return; } // Reset the camera's position on the main thread, // because the Camera class is not thread-safe and // it's possible for its state to not get updated diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 68ba8fbae..ad412a92b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Networking string name, Either addressOrAccountId, string reason, - DateTime? expiration) + Option expiration) { this.Name = name; this.AddressOrAccountId = addressOrAccountId; @@ -94,8 +94,9 @@ namespace Barotrauma.Networking topArea.ForceLayoutRecalculation(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), - bannedPlayer.ExpirationTime == null ? - TextManager.Get("BanPermanent") : TextManager.GetWithVariable("BanExpires", "[time]", bannedPlayer.ExpirationTime.Value.ToString()), + bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime) + ? TextManager.GetWithVariable("BanExpires", "[time]", expirationTime.ToLocalUserString()) + : TextManager.Get("BanPermanent"), font: GUIStyle.SmallFont); LocalizedString reason = TextManager.GetServerMessage(bannedPlayer.Reason).Fallback(bannedPlayer.Reason); @@ -149,11 +150,11 @@ namespace Barotrauma.Networking bool includesExpiration = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - DateTime? expiration = null; + Option expiration = Option.None(); if (includesExpiration) { double hoursFromNow = incMsg.ReadDouble(); - expiration = DateTime.Now + TimeSpan.FromHours(hoursFromNow); + expiration = Option.Some(SerializableDateTime.LocalNow + TimeSpan.FromHours(hoursFromNow)); } string reason = incMsg.ReadString(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 4ae286687..cab954d3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -26,8 +26,8 @@ namespace Barotrauma.Networking if (type != ChatMessageType.Order) { changeType = (PlayerConnectionChangeType)msg.ReadByte(); - txt = msg.ReadString(); } + txt = msg.ReadString(); string senderName = msg.ReadString(); Character senderCharacter = null; @@ -87,11 +87,6 @@ namespace Barotrauma.Networking targetRoom = senderCharacter?.CurrentHull?.DisplayName?.Value; } - txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom, - givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, - orderOption: orderOption, - isNewOrder: orderMessageInfo.IsNewOrder); - if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { Order order = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 54a932e0b..c51b66457 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -82,6 +82,11 @@ namespace Barotrauma.Networking Initialization = ConnectionInitialization.SteamTicketAndVersion }; + if (steamAuthTicket is { Canceled: true }) + { + throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled."); + } + ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket { Name = GameMain.Client.Name, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 7698c4b66..bf262be14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -120,6 +120,7 @@ namespace Barotrauma.Networking client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; + client.RadioNoise = 0.0f; if (messageType == ChatMessageType.Radio) { client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier); @@ -131,7 +132,6 @@ namespace Barotrauma.Networking } else { - client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier); } client.VoipSound.UseMuffleFilter = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 60dc7f6ea..0bc8adcd3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Barotrauma.IO; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -58,7 +59,7 @@ namespace Barotrauma var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") { - UserData = saveInfo.FilePath + UserData = saveInfo }; var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath), @@ -87,10 +88,9 @@ namespace Barotrauma }; string saveTimeStr = string.Empty; - if (saveInfo.SaveTime > 0) + if (saveInfo.SaveTime.TryUnwrap(out var time)) { - DateTime time = ToolBox.Epoch.ToDateTime(saveInfo.SaveTime); - saveTimeStr = time.ToString(); + saveTimeStr = time.ToLocalUserString(); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) @@ -102,6 +102,26 @@ namespace Barotrauma return saveFrame; } + protected void SortSaveList() + { + saveList.Content.RectTransform.SortChildren((c1, c2) => + { + if (c1.GUIComponent.UserData is not CampaignMode.SaveInfo file1 + || c2.GUIComponent.UserData is not CampaignMode.SaveInfo file2) + { + return 0; + } + + if (!file1.SaveTime.TryUnwrap(out var file1WriteTime) + || !file2.SaveTime.TryUnwrap(out var file2WriteTime)) + { + return 0; + } + + return file2WriteTime.CompareTo(file1WriteTime); + }); + } + public struct CampaignSettingElements { public SettingValue TutorialEnabled; @@ -367,5 +387,25 @@ namespace Barotrauma return inputContainer; } } + + public abstract void UpdateLoadMenu(IEnumerable saveFiles = null); + + protected bool DeleteSave(GUIButton button, object obj) + { + if (obj is not CampaignMode.SaveInfo saveInfo) { return false; } + + var header = TextManager.Get("deletedialoglabel"); + var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveInfo.FilePath)); + + EventEditorScreen.AskForConfirmation(header, body, () => + { + SaveUtil.DeleteSave(saveInfo.FilePath); + prevSaveFiles?.RemoveAll(s => s.FilePath == saveInfo.FilePath); + UpdateLoadMenu(prevSaveFiles.ToList()); + return true; + }); + + return true; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 1a19fc973..c86f3c04e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -192,7 +192,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -220,37 +220,16 @@ namespace Barotrauma CreateSaveElement(saveInfo); } - saveList.Content.RectTransform.SortChildren((c1, c2) => - { - string file1 = c1.GUIComponent.UserData as string; - string file2 = c2.GUIComponent.UserData as string; - DateTime file1WriteTime = DateTime.MinValue; - DateTime file2WriteTime = DateTime.MinValue; - try - { - file1WriteTime = File.GetLastWriteTime(file1); - } - catch - { - //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list - }; - try - { - file2WriteTime = File.GetLastWriteTime(file2); - } - catch - { - //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list - }; - return file2WriteTime.CompareTo(file1WriteTime); - }); + SortSaveList(); loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) { OnClicked = (btn, obj) => { - if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } - LoadGame?.Invoke(saveList.SelectedData as string); + if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } + if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } + LoadGame?.Invoke(saveInfo.FilePath); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); return true; }, @@ -264,37 +243,20 @@ namespace Barotrauma }; } + private bool SelectSaveFile(GUIComponent component, object obj) { - string fileName = (string)obj; + if (obj is not CampaignMode.SaveInfo saveInfo) { return true; } + string fileName = saveInfo.FilePath; loadGameButton.Enabled = true; deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; if (deleteMpSaveButton.Visible) { - deleteMpSaveButton.UserData = obj as string; + deleteMpSaveButton.UserData = saveInfo; } return true; } - - private bool DeleteSave(GUIButton button, object obj) - { - string saveFile = obj as string; - if (obj == null) { return false; } - - var header = TextManager.Get("deletedialoglabel"); - var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); - - EventEditorScreen.AskForConfirmation(header, body, () => - { - SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile); - UpdateLoadMenu(prevSaveFiles.ToList()); - return true; - }); - - return true; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index c13194e4e..a3ae05b14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -581,7 +581,7 @@ namespace Barotrauma } } - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -649,46 +649,27 @@ namespace Barotrauma } } - saveList.Content.RectTransform.SortChildren((c1, c2) => - { - string file1 = c1.GUIComponent.UserData as string; - string file2 = c2.GUIComponent.UserData as string; - DateTime file1WriteTime = DateTime.MinValue; - DateTime file2WriteTime = DateTime.MinValue; - try - { - file1WriteTime = File.GetLastWriteTime(file1); - } - catch - { - //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list - }; - try - { - file2WriteTime = File.GetLastWriteTime(file2); - } - catch - { - //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list - }; - return file2WriteTime.CompareTo(file1WriteTime); - }); + SortSaveList(); loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) { OnClicked = (btn, obj) => { - if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } - LoadGame?.Invoke(saveList.SelectedData as string); + if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } + if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } + LoadGame?.Invoke(saveInfo.FilePath); + return true; }, Enabled = false }; - } - + } + private bool SelectSaveFile(GUIComponent component, object obj) { - string fileName = (string)obj; + if (obj is not CampaignMode.SaveInfo saveInfo) { return true; } + + string fileName = saveInfo.FilePath; XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); if (doc?.Root == null) @@ -701,72 +682,55 @@ namespace Barotrauma RemoveSaveFrame(); - string subName = doc.Root.GetAttributeString("submarine", ""); - string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); - DateTime? time = null; - if (long.TryParse(saveTime, out long unixTime)) - { - time = ToolBox.Epoch.ToDateTime(unixTime); - saveTime = time.ToString(); - } + string subName = saveInfo.SubmarineName; + LocalizedString saveTime = saveInfo.SaveTime + .Select(t => (LocalizedString)t.ToLocalUserString()) + .Fallback(TextManager.Get("Unknown")); string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); - var saveFileFrame = new GUIFrame(new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) - { - RelativeOffset = new Vector2(0.0f, 0.1f) - }, style: "InnerFrame") + var saveFileFrame = new GUIFrame( + new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) + { + RelativeOffset = new Vector2(0.0f, 0.1f) + }, style: "InnerFrame") { UserData = "savefileframe" }; - var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) - { - RelativeOffset = new Vector2(0, 0.05f) - }, + var titleText = new GUITextBlock( + new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) + { + RelativeOffset = new Vector2(0, 0.05f) + }, Path.GetFileNameWithoutExtension(fileName), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); - var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) - { - RelativeOffset = new Vector2(0, 0.1f) - }); + var layoutGroup = new GUILayoutGroup( + new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) + { + RelativeOffset = new Vector2(0, 0.1f) + }); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) }, TextManager.Get("Delete"), style: "GUIButtonSmall") { - UserData = fileName, + UserData = saveInfo, OnClicked = DeleteSave }; return true; } - private bool DeleteSave(GUIButton button, object obj) - { - string saveFile = obj as string; - if (obj == null) { return false; } - - LocalizedString header = TextManager.Get("deletedialoglabel"); - LocalizedString body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); - - EventEditorScreen.AskForConfirmation(header, body, () => - { - SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile); - UpdateLoadMenu(prevSaveFiles.ToList()); - return true; - }); - - return true; - } - private void RemoveSaveFrame() { GUIComponent prevFrame = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index f8450df3b..00591d5b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -551,6 +551,7 @@ namespace Barotrauma submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; case CampaignMode.InteractionType.Map: + GameMain.GameSession?.Map?.ResetPendingSub(); //refresh mission rewards (may have been changed by e.g. a pending submarine switch) foreach (GUITextBlock rewardText in missionRewardTexts) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 78ef8b08a..102992cae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -25,14 +25,11 @@ namespace Barotrauma.CharacterEditor { get { - if (cam == null) + cam ??= new Camera() { - cam = new Camera() - { - MinZoom = 0.1f, - MaxZoom = 5.0f - }; - } + MinZoom = 0.1f, + MaxZoom = 5.0f + }; return cam; } } @@ -262,7 +259,10 @@ namespace Barotrauma.CharacterEditor #endif } GameMain.Instance.ResolutionChanged -= OnResolutionChanged; - GameMain.LightManager.LightingEnabled = true; + if (!GameMain.DevMode) + { + GameMain.LightManager.LightingEnabled = true; + } ClearWidgets(); ClearSelection(); } @@ -280,6 +280,7 @@ namespace Barotrauma.CharacterEditor #region Main methods public override void AddToGUIUpdateList() { + if (rightArea == null || leftArea == null) { return; } rightArea.AddToGUIUpdateList(); leftArea.AddToGUIUpdateList(); @@ -778,7 +779,7 @@ namespace Barotrauma.CharacterEditor scaledMouseSpeed = PlayerInput.MouseSpeedPerSecond * (float)deltaTime; Cam.UpdateTransform(true); Submarine.CullEntities(Cam); - Submarine.MainSub.UpdateTransform(); + Submarine.MainSub?.UpdateTransform(); // Lightmaps if (GameMain.LightManager.LightingEnabled) @@ -1570,10 +1571,7 @@ namespace Barotrauma.CharacterEditor { wayPoint = WayPoint.GetRandom(spawnType: SpawnType.Human, sub: Submarine.MainSub); } - if (wayPoint == null) - { - wayPoint = WayPoint.GetRandom(sub: Submarine.MainSub); - } + wayPoint ??= WayPoint.GetRandom(sub: Submarine.MainSub); spawnPosition = wayPoint.WorldPosition; } @@ -3998,7 +3996,7 @@ namespace Barotrauma.CharacterEditor }; }).Draw(spriteBatch, deltaTime); } - else + else if (groundedParams != null) { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { @@ -4107,7 +4105,7 @@ namespace Barotrauma.CharacterEditor }; }).Draw(spriteBatch, deltaTime); } - else + else if (groundedParams != null) { GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index dd9aba825..ac0be6534 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; using System.Diagnostics; @@ -27,7 +26,7 @@ namespace Barotrauma public Effect ThresholdTintEffect { get; private set; } public Effect BlueprintEffect { get; set; } - public GameScreen(GraphicsDevice graphics, ContentManager content) + public GameScreen(GraphicsDevice graphics) { cam = new Camera(); cam.Translate(new Vector2(-10.0f, 50.0f)); @@ -38,20 +37,13 @@ namespace Barotrauma CreateRenderTargets(graphics); }; - Effect LoadEffect(string path) - => content.Load(path -#if LINUX || OSX - +"_opengl" -#endif - ); - //var blurEffect = LoadEffect("Effects/blurshader"); - damageEffect = LoadEffect("Effects/damageshader"); - PostProcessEffect = LoadEffect("Effects/postprocess"); - GradientEffect = LoadEffect("Effects/gradientshader"); - GrainEffect = LoadEffect("Effects/grainshader"); - ThresholdTintEffect = LoadEffect("Effects/thresholdtint"); - BlueprintEffect = LoadEffect("Effects/blueprintshader"); + damageEffect = EffectLoader.Load("Effects/damageshader"); + PostProcessEffect = EffectLoader.Load("Effects/postprocess"); + GradientEffect = EffectLoader.Load("Effects/gradientshader"); + GrainEffect = EffectLoader.Load("Effects/grainshader"); + ThresholdTintEffect = EffectLoader.Load("Effects/thresholdtint"); + BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); damageEffect.Parameters["xStencil"].SetValue(damageStencil); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 284a9634a..107c22358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -994,7 +994,7 @@ namespace Barotrauma || item.IsDownloadPending || (item.InstallTime.TryGetValue(out var workshopInstallTime) && pkg.InstallTime.TryUnwrap(out var localInstallTime) - && localInstallTime < workshopInstallTime))); + && localInstallTime.ToUtcValue() < workshopInstallTime))); modUpdateStatus = (DateTime.Now + ModUpdateInterval, count); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index e36471206..a5e37dcdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -1,6 +1,5 @@ #nullable enable using System.Linq; -using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -34,7 +33,7 @@ namespace Barotrauma { BlueprintEffect.Dispose(); GameMain.Instance.Content.Unload(); - BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); + BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); GameMain.GameScreen.BlueprintEffect = BlueprintEffect; return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index c3c4c3873..4df190a1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -9,7 +9,7 @@ using System.Diagnostics; namespace Barotrauma { - class SerializableEntityEditor : GUIComponent + sealed class SerializableEntityEditor : GUIComponent { private readonly int elementHeight; private readonly GUILayoutGroup layoutGroup; @@ -399,10 +399,6 @@ namespace Barotrauma { propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); } - else if (value is string stringVal) - { - propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip); - } else if (value.GetType().IsEnum) { if (value.GetType().IsDefined(typeof(FlagsAttribute), inherit: false)) @@ -450,6 +446,10 @@ namespace Barotrauma { propertyField = CreateStringArrayField(entity, property, a, displayName, toolTip); } + else if (value is string or Identifier) + { + propertyField = CreateStringField(entity, property, value.ToString(), displayName, toolTip); + } return propertyField; } @@ -696,7 +696,7 @@ namespace Barotrauma propertyBox.OnEnterPressed += (box, text) => OnApply(box); refresh += () => { - if (!propertyBox.Selected) { propertyBox.Text = (string)property.GetValue(entity); } + if (!propertyBox.Selected) { propertyBox.Text = property.GetValue(entity).ToString(); } }; bool OnApply(GUITextBox textBox) @@ -714,7 +714,7 @@ namespace Barotrauma if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); - textBox.Text = (string) property.GetValue(entity); + textBox.Text = property.GetValue(entity).ToString(); textBox.Flash(GUIStyle.Green, flashDuration: 1f); } //restore the entities that were selected before applying diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index 7b89b66bc..6287642f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -42,12 +42,7 @@ namespace Barotrauma { if (effect == null) { -#if WINDOWS - effect = GameMain.Instance.Content.Load("Effects/deformshader"); -#endif -#if LINUX || OSX - effect = GameMain.Instance.Content.Load("Effects/deformshader_opengl"); -#endif + effect = EffectLoader.Load("Effects/deformshader"); } Invert = invert; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index b8688c2f8..1089221b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -202,6 +202,7 @@ namespace Barotrauma.Steam ContentPackageManager.EnabledPackages.Core!, (p) => { }, heightScale: 1.0f / 13.0f); + enabledCoreDropdown.AllowNonText = true; Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); topRight.ChildAnchor = Anchor.CenterLeft; @@ -535,34 +536,119 @@ namespace Barotrauma.Steam bulkUpdateButton.Enabled = false; bulkUpdateButton.ToolTip = ""; ContentPackageManager.UpdateContentPackageList(); - + + var corePackages = ContentPackageManager.CorePackages.ToArray(); + var currentCore = ContentPackageManager.EnabledPackages.Core!; SwapDropdownValues(enabledCoreDropdown, (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, + corePackages, + currentCore, (p) => { + // Manually set dropdown text because + // adding buttons to the elements breaks + // this part of the dropdown code + enabledCoreDropdown.Text = p.Name; enabledCoreDropdown.ButtonTextColor = p.HasAnyErrors ? GUIStyle.Red : GUIStyle.TextColorNormal; }); - enabledCoreDropdown.ListBox.Content.Children - .OfType() - .ForEach(tb => - CreateModErrorInfo( - (tb.UserData as ContentPackage)!, - tb, - tb)); - - void addRegularModToList(RegularPackage mod, GUIListBox list) + + void addButtonForMod(ContentPackage mod, GUILayoutGroup parent) { - var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), + if (ContentPackageManager.LocalPackages.Contains(mod)) + { + var editButton = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.EditButton") + { + OnClicked = (button, o) => + { + ToolBox.OpenFileWithShell(mod.Dir); + return false; + }, + ToolTip = TextManager.Get("OpenLocalModInExplorer") + }; + } + else if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + var infoButton = new GUIButton( + new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: null) + { + CanBeSelected = false, + OnClicked = (button, o) => + { + PrepareToShowModInfo(mod); + return false; + } + }; + if (!SteamManager.IsInitialized) + { + infoButton.Enabled = false; + } + TaskPool.AddIfNotFound( + $"DetermineUpdateRequired{mod.UgcId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (isUpToDate) { return; } + + infoButton.CanBeSelected = true; + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); + bulkUpdateButton.Enabled = true; + bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + }); + } + } + + GUILayoutGroup createBaseModListUi(ContentPackage mod, GUIListBox listBox, float height) + { + var modFrame = new GUIFrame(new RectTransform((1.0f, height), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = mod }; + + var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) + { + CanBeFocused = false + }; + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), + text: mod.Name) + { + CanBeFocused = false + }; + CreateModErrorInfo(mod, modFrame, modName); + addButtonForMod(mod, frameContent); + return frameContent; + } + + foreach (var element in enabledCoreDropdown.ListBox.Content.Children.ToArray()) + { + enabledCoreDropdown.ListBox.RemoveChild(element); + if (element.UserData is not ContentPackage mod) { continue; } + + createBaseModListUi(mod, enabledCoreDropdown.ListBox, 0.24f); + } + enabledCoreDropdown.Select(corePackages.IndexOf(currentCore)); + + void addRegularModToList(RegularPackage mod, GUIListBox list) + { + var frameContent = createBaseModListUi(mod, list, 0.08f); + + var modFrame = frameContent.Parent; + var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), onUpdate: (f, component) => { @@ -639,76 +725,13 @@ namespace Barotrauma.Steam contextMenuOptions.ToArray()); } }); - - var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - + var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIDragIndicator") { CanBeFocused = false }; - - var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) - { - CanBeFocused = false - }; - var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), - text: mod.Name) - { - CanBeFocused = false - }; - CreateModErrorInfo(mod, modFrame, modName); - if (ContentPackageManager.LocalPackages.Contains(mod)) - { - var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: "WorkshopMenu.EditButton") - { - OnClicked = (button, o) => - { - ToolBox.OpenFileWithShell(mod.Dir); - return false; - }, - ToolTip = TextManager.Get("OpenLocalModInExplorer") - }; - } - else if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - var infoButton = new GUIButton( - new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: null) - { - CanBeSelected = false, - OnClicked = (button, o) => - { - PrepareToShowModInfo(mod); - return false; - } - }; - if (!SteamManager.IsInitialized) - { - infoButton.Enabled = false; - } - TaskPool.AddIfNotFound( - $"DetermineUpdateRequired{mod.UgcId}", - mod.IsUpToDate(), - t => - { - if (!t.TryGetResult(out bool isUpToDate)) { return; } - - if (!isUpToDate) - { - infoButton.CanBeSelected = true; - infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); - infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); - bulkUpdateButton.Enabled = true; - bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); - } - }); - } + dragIndicator.RectTransform.SetAsFirstChild(); } void addRegularModsToList(IEnumerable mods, GUIListBox list) @@ -729,7 +752,7 @@ namespace Barotrauma.Steam .Where(p => ContentPackageManager.RegularPackages.Contains(p))) .ToArray(); var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); - + addRegularModsToList(enabledMods, enabledRegularModsList); if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } @@ -747,7 +770,7 @@ namespace Barotrauma.Steam var mod = child.UserData as RegularPackage; if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; } - if (!(ugcId is SteamWorkshopId workshopId)) { continue; } + if (ugcId is not SteamWorkshopId workshopId) { continue; } var btn = child.GetChild()?.GetAllChildren().Last(); if (btn is null) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index d99e8887b..fac6cd8cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -68,10 +68,13 @@ namespace Barotrauma.Steam } protected static void SwapDropdownValues( - GUIDropDown dropdown, Func textFunc, IReadOnlyList values, T currentValue, + GUIDropDown dropdown, + Func textFunc, + IReadOnlyList values, + T currentValue, Action setter) { - if (dropdown.ListBox.Content.Children.Any(c => !(c.UserData is T))) + if (dropdown.ListBox.Content.Children.Any(c => c.UserData is not T)) { throw new Exception("SwapValues must preserve the type of the dropdown's userdata"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs new file mode 100644 index 000000000..5a34c929f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/EffectLoader.cs @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework.Graphics; +namespace Barotrauma; + +static class EffectLoader +{ + public static Effect Load(string path) + => GameMain.Instance.Content.Load(path +#if LINUX || OSX + +"_opengl" +#endif + ); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index f0315b696..f7c0b35fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -9,7 +9,7 @@ namespace Barotrauma { sealed class SpriteRecorder : ISpriteBatch, IDisposable { - private readonly record struct Command( + public readonly record struct Command( Texture2D Texture, VertexPositionColorTexture VertexBL, VertexPositionColorTexture VertexBR, @@ -346,7 +346,7 @@ namespace Barotrauma } recordedBuffers.Clear(); commandList.Clear(); - indexBuffer.Dispose(); indexBuffer = null; + indexBuffer?.Dispose(); indexBuffer = null; ReadyToRender = false; } } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb new file mode 100644 index 0000000000000000000000000000000000000000..1f290fc5c1ed740460bf482de9f1bb57fc3dbc8f GIT binary patch literal 2056 zcmbtVO>Y}j6g~6d+6@Y}kU*?pKwVlEX#})Lp-JpGMUce!qjC`vam<)nHJ)f}Hw{8) z1Zz-Ntlb@uO;_w$vW5j>!vZTmAV@4R=e(IwOcdDgq&M&0bI<*F_dU&P?`|Gtkjnwq z@lL-R2BV-iF13f9QZ?*+>>mZ;{Zcs?j{V`dwAJhR-ErOTeCUToZzmWA+oYTmi{2eS z9QA|Y+G^?b(n@LN#?7Kvo(#s5&|e$+lX2J?6usKyV9@Wr?H@OT`~GlkI2jC}VI60I zo$YEnXZ^7H)E^f1;Rr6kuED~mSAa@;v;5oc@2~#4{L4!}ef#z4mw*0sYLO$G%k>q` zZ*1V_3~-y+Mlig2NL>^5d|u>Qk%%I{k}8z1B{FkaiUyO{2*@zAs^FmiyFkW8ehNgl z43>OeR7iqA{z)p+FO?r5m!Bs1Ba1S#kZ(tf{5Y1a$VJQG^ouOao(Q+#633L=aV+GB zLvu>ZjrA6F`OU?1ZCKR5n-Z7DaeieY~yFsPZ+t(XGMT`#+SNH zU9!O#*P%Y6t_jB>UtAiP8@8M1k{$Y{ey-Vf%kVZ$eR&hd%FU+IE#z6tEJ}5ox^kpC z`_G!sb%(l>=u0M^dN8NYJd&<`IFwbpboB_-)svL(8h*S_o}uBA|9Ar+Egz+I>RiA& zq7rI z=$pAtyYf_%9Ktpe%vu*ey2a?ZXNakWs-5t^@MJW;va5#6?7}#*XGzTG+_=^%SJ}3J ztFp0;s3x51XK@boPPL^R$)`(6{(s@{o|`zPmJ6I!)}ei9ypiP8Dj)9q%IArVeH6v? zkc+8Z@xFihziYoNXIi^#s{Jy~VqHrY>RYs_R`np|T(l{6YNL2H-D`?1-VfTbve1u| z6Q4P-4A)7qbPw`L?}^UHjQx1Mp+0#@f6_j^5ZC)AugP1D?OiC~Apuu;$+mGK@Sj&mpxThB!R;Jb& KjaI-P7VtMmb_@Xk literal 0 HcmV?d00001 diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb new file mode 100644 index 0000000000000000000000000000000000000000..c0ca155d0d16898e275fb4876e175c6835be5e8d GIT binary patch literal 1842 zcmbuA&2G~`5XaX|LrbJcy>MTtmlT5Kk4S(}Bt+Wu07Vr^Q6*GGuDvd++Pl%(&KKeV zka!DTinoE8b^MW}J+TMNJM-K9&&xSYNO#RbJGk zm)UK^{SRz8miJ8bMHWQ_hw=LfAs6Sv>q9d9`G*-t(`1qf9!eD_lNs}S???kNO7<*b zkY7zgj(Hfcke+{>oQ>M8sbW4yc^b2TXYrJVk@V8H4|%vi`!N#%58JJFYb``khi(gh z?cD(EOOd1{09ap7I+kg5Y4xy8>jdaxBW2Ymc75X1CvJVxgUK;u#s$M*d}}u(>t++N zcM*FFadr`B3vqW57m%Vli5E{Jrkqp663pyjk51E5OD^v6g3X2sdT1acsWwxaS+wSZ zvTdvyv&BG46`)eB9w{s;T^k-bu;^Vhf!4j#VtBMO$padDTy##_t?%vDQ;1zz<`T~D zB^K8;7qI=>x&SL0d*-Sz+U5dTu%;HgR_4{bg6D#APrX7^AyLva>5n92~x?64= z8%Dc9E8kNG_yQLz*s*f2BLVk<7Va`646j@cuLV#A4Y{rXj8psT`S!H1c;b61C{A_vgy1U%Lb}Iy)#-C|&hR2) zqvAbV#|4%jc+K|U+f16^ORNSljQrK!H^8{pcs6IA@@^w`H5", Scope = "member", Target = "~M:Barotrauma.GameSettings.CreateSettingsFrame")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "IDE0047")] + diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 7bf8666f0..5c27ba425 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 36467864b..aec4b86f1 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb index 7d48e26be..57132f551 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb @@ -79,3 +79,8 @@ /processorParam:DebugMode=Auto /build:blueprintshader.fx +#begin wearableclip.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:wearableclip.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index 82d54dedf..c5b56f9eb 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -78,3 +78,9 @@ /processor:EffectProcessor /processorParam:DebugMode=Auto /build:thresholdtint_opengl.fx + +#begin wearableclip_opengl.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:wearableclip_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx new file mode 100644 index 000000000..4ea589cf4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx @@ -0,0 +1,42 @@ + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float stencilSample(float2 texCoord, float2 offset) +{ + return xStencil.Sample( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * color; + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx new file mode 100644 index 000000000..25dd7f3d3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx @@ -0,0 +1,42 @@ + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float stencilSample(float2 texCoord, float2 offset) +{ + return tex2D( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * color; + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_2_0 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 671b920b4..adc6b7153 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 422aaffe1..5ee5b2bc2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index be8a82f6f..6692e0d12 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 5aca9fbf7..a8b32422a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -14,6 +14,13 @@ namespace Barotrauma /// public bool Discarded; + public void ApplyDeathEffects() + { + RespawnManager.ReduceCharacterSkills(this); + RemoveSavedStatValuesOnDeath(); + CauseOfDeath = null; + } + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (Character == null || Character.Removed) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs new file mode 100644 index 000000000..5842632f7 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Health/HealingCooldownServer.cs @@ -0,0 +1,51 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal static class HealingCooldown + { + private static readonly Dictionary HealingCooldowns = new(); + + // Little bit less than client's 0.5 second cooldown to account for latency + private const float CooldownDuration = 0.4f; + + public static bool IsOnCooldown(Client client) + { + RemoveExpiredCooldowns(); + return HealingCooldowns.ContainsKey(client); + } + + public static void SetCooldown(Client client) + { + RemoveExpiredCooldowns(); + DateTimeOffset newCooldown = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); + HealingCooldowns[client] = newCooldown; + } + + private static void RemoveExpiredCooldowns() + { + HashSet? expiredCooldowns = null; + + DateTimeOffset now = DateTimeOffset.UtcNow; + + foreach (var (client, cooldown) in HealingCooldowns) + { + if (now < cooldown) { continue; } + + expiredCooldowns ??= new HashSet(); + expiredCooldowns.Add(client); + } + + if (expiredCooldowns is null) { return; } + + foreach (Client expiredCooldown in expiredCooldowns) + { + HealingCooldowns.Remove(expiredCooldown); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 138dc3cfe..d99340d6b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -109,15 +109,22 @@ namespace Barotrauma return AccountId == other.AccountId && other.ClientAddress == ClientAddress; } + public void Reset() + { + itemData = null; + healthData = null; + WalletData = null; + } + public void SpawnInventoryItems(Character character, Inventory inventory) { if (character == null) { - throw new System.InvalidOperationException($"Failed to spawn inventory items. Character was null."); + throw new InvalidOperationException($"Failed to spawn inventory items. Character was null."); } if (itemData == null) { - throw new System.InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); + throw new InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); } character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 089758334..82810135f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -240,9 +240,7 @@ namespace Barotrauma //reduce skills if the character has died if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { - RespawnManager.ReduceCharacterSkills(characterInfo); - characterInfo.RemoveSavedStatValuesOnDeath(); - characterInfo.CauseOfDeath = null; + characterInfo.ApplyDeathEffects(); } c.CharacterInfo = characterInfo; SetClientCharacterData(c); @@ -254,13 +252,21 @@ namespace Barotrauma { if (data.HasSpawned && !GameMain.Server.ConnectedClients.Any(c => data.MatchesClient(c))) { - var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); - if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) + var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); + if (character != null && + (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) { + //character still alive (or killed by Disconnect) -> save it as-is characterData.RemoveAll(cd => cd.IsDuplicate(data)); data.Refresh(character); characterData.Add(data); } + else + { + //character dead or removed -> reduce skills, remove items, health data, etc + data.CharacterInfo.ApplyDeathEffects(); + data.Reset(); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 14901a971..65b176cff 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; @@ -19,7 +20,7 @@ namespace Barotrauma bool accessible = c.Character.CanAccessInventory(this); if (this is CharacterInventory characterInventory && accessible) { - if (Owner == null || !(Owner is Character ownerCharacter)) + if (Owner == null || Owner is not Character ownerCharacter) { accessible = false; } @@ -39,7 +40,7 @@ namespace Barotrauma { foreach (ushort id in newItemIDs[i]) { - if (!(Entity.FindEntityByID(id) is Item item)) { continue; } + if (Entity.FindEntityByID(id) is not Item item) { continue; } item.PositionUpdateInterval = 0.0f; if (item.ParentInventory != null && item.ParentInventory != this) { @@ -94,7 +95,15 @@ namespace Barotrauma { foreach (ushort id in newItemIDs[i]) { - if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } + 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()) + { + DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})"); + continue; + } if (GameMain.Server != null) { @@ -105,7 +114,7 @@ namespace Barotrauma (c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory))) { #if DEBUG || UNSTABLE - DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {(item.ParentInventory?.Owner.ToString() ?? "null")}). No access.", Color.Yellow); + DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); #endif if (item.body != null && !c.PendingPositionUpdates.Contains(item)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index e04c02d33..3508a03ae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -153,25 +153,27 @@ namespace Barotrauma (components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c); break; case EventType.Treatment: - if (c.Character == null || !c.Character.CanInteractWith(this)) return; + if (c.Character == null || !c.Character.CanInteractWith(this)) { return; } UInt16 characterID = msg.ReadUInt16(); byte limbIndex = msg.ReadByte(); - Character targetCharacter = FindEntityByID(characterID) as Character; - if (targetCharacter == null) break; - if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) break; + if (HealingCooldown.IsOnCooldown(c)) { return; } + if (FindEntityByID(characterID) is not Character targetCharacter) { break; } + if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) { break; } + + HealingCooldown.SetCooldown(c); Limb targetLimb = limbIndex < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[limbIndex] : null; - if (ContainedItems == null || ContainedItems.All(i => i == null)) + if (ContainedItems == null || ContainedItems.All(static i => i == null)) { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " used item " + Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} used item {Name}", ServerLog.MessageType.ItemInteraction); } else { GameServer.Log( - GameServer.CharacterLogName(c.Character) + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", + $"{GameServer.CharacterLogName(c.Character)} used item {Name} (contained items: {string.Join(", ", ContainedItems.Select(i => i.Name))})", ServerLog.MessageType.ItemInteraction); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 9febcddaf..c2a1f8cc6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -11,10 +11,10 @@ namespace Barotrauma.Networking { private static UInt32 LastIdentifier = 0; - public bool Expired => ExpirationTime is { } expirationTime && DateTime.Now > expirationTime; + public bool Expired => ExpirationTime.TryUnwrap(out var expirationTime) && SerializableDateTime.LocalNow > expirationTime; public BannedPlayer( - string name, Either addressOrAccountId, string reason, DateTime? expirationTime) + string name, Either addressOrAccountId, string reason, Option expirationTime) { this.Name = name; this.AddressOrAccountId = addressOrAccountId; @@ -39,6 +39,7 @@ namespace Barotrauma.Networking { LoadBanList(); } + RemoveExpired(); } private void LoadLegacyBanList() @@ -69,7 +70,7 @@ namespace Barotrauma.Networking { if (DateTime.TryParse(separatedLine[2], out DateTime parsedTime)) { - expirationTime = parsedTime; + expirationTime = DateTime.SpecifyKind(parsedTime, DateTimeKind.Local); } else { @@ -80,15 +81,18 @@ namespace Barotrauma.Networking } string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : ""; - if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) { continue; } + var serializableExpirationTime + = expirationTime.HasValue + ? Option.Some(new SerializableDateTime(expirationTime.Value)) + : Option.None(); if (AccountId.Parse(endpointStr).TryUnwrap(out var accountId)) { - bannedPlayers.Add(new BannedPlayer(name, accountId, reason, expirationTime)); + bannedPlayers.Add(new BannedPlayer(name, accountId, reason, serializableExpirationTime)); } else if (Address.Parse(endpointStr).TryUnwrap(out var address)) { - bannedPlayers.Add(new BannedPlayer(name, address, reason, expirationTime)); + bannedPlayers.Add(new BannedPlayer(name, address, reason, serializableExpirationTime)); } } @@ -109,10 +113,22 @@ namespace Barotrauma.Networking var name = element.GetAttributeString("name", "")!; var reason = element.GetAttributeString("reason", "")!; - DateTime? expirationTime = DateTime.FromBinary(unchecked((long)element.GetAttributeUInt64("expirationtime", 0))); - - if (expirationTime < DateTime.Now) { expirationTime = null; } - + var expirationTime = Option.None(); + var expirationTimeStr = element.GetAttributeString("expirationtime", "")!; + + if (UInt64.TryParse(expirationTimeStr, out var binaryDateTime) && binaryDateTime > 0) + { + // Backwards compatibility: if expirationtime is stored as an int, + // convert to SerializableDateTime with local timezone because + // banlists used to assume local time + expirationTime = Option.Some( + new SerializableDateTime( + DateTime.FromBinary((long)binaryDateTime), + SerializableTimeZone.LocalTimeZone)); + } + + expirationTime = expirationTime.Fallback(SerializableDateTime.Parse(expirationTimeStr)); + if (accountId.IsNone() && address.IsNone()) { return Option.None(); } Either addressOrAccountId = accountId.TryUnwrap(out var accId) @@ -171,14 +187,14 @@ namespace Barotrauma.Networking string logMsg = "Banned " + name; if (!string.IsNullOrEmpty(reason)) { logMsg += ", reason: " + reason; } - if (duration.HasValue) { logMsg += ", duration: " + duration.Value.ToString(); } + if (duration.HasValue) { logMsg += ", duration: " + duration.Value; } DebugConsole.Log(logMsg); - DateTime? expirationTime = null; + Option expirationTime = Option.None(); if (duration.HasValue) { - expirationTime = DateTime.Now + duration.Value; + expirationTime = Option.Some(new SerializableDateTime(DateTime.Now + duration.Value)); } bannedPlayers.Add(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); @@ -232,9 +248,10 @@ namespace Barotrauma.Networking { retVal.SetAttributeValue("address", address.StringRepresentation); } - if (bannedPlayer.ExpirationTime is { } expirationTime) + if (bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime)) { - retVal.SetAttributeValue("expirationtime", unchecked((ulong)expirationTime.ToBinary())); + #warning TODO: stop writing binary DateTime representation after this gets on main + retVal.SetAttributeValue("expirationtime", expirationTime.ToLocalValue().ToBinary()); } return retVal; @@ -269,11 +286,11 @@ namespace Barotrauma.Networking outMsg.WriteString(bannedPlayer.Name); outMsg.WriteUInt32(bannedPlayer.UniqueIdentifier); - outMsg.WriteBoolean(bannedPlayer.ExpirationTime != null); + outMsg.WriteBoolean(bannedPlayer.ExpirationTime.IsSome()); outMsg.WritePadBits(); - if (bannedPlayer.ExpirationTime != null) + if (bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime)) { - double hoursFromNow = (bannedPlayer.ExpirationTime.Value - DateTime.Now).TotalHours; + double hoursFromNow = (expirationTime.ToUtcValue() - DateTime.UtcNow).TotalHours; outMsg.WriteDouble(hoursFromNow); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index a12693b51..8c9a90c95 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -140,6 +140,7 @@ namespace Barotrauma.Networking ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); KarmaManager.SelectPreset(ServerSettings.KarmaPreset); ServerSettings.SetPassword(password); + ServerSettings.SaveSettings(); Voting = new Voting(); @@ -3227,16 +3228,16 @@ namespace Barotrauma.Networking } //too far to hear the msg -> don't send - if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; + if (string.IsNullOrWhiteSpace(modifiedMessage)) { continue; } } break; case ChatMessageType.Dead: //character still alive -> don't send - if (client != senderClient && client.Character != null && !client.Character.IsDead) continue; + if (client != senderClient && client.Character != null && !client.Character.IsDead) { continue; } break; case ChatMessageType.Private: //private msg sent to someone else than this client -> don't send - if (client != targetClient && client != senderClient) continue; + if (client != targetClient && client != senderClient) { continue; } break; } @@ -3272,11 +3273,17 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + if (ChatMessage.CanUseRadio(message.Sender, out var senderRadio)) + { + //send to chat-linked wifi components + Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item); + senderRadio.TransmitSignal(s, sentFromChat: true); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index a02d01ebe..600503651 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -10,6 +10,7 @@ namespace Barotrauma.Networking segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + msg.WriteString(Text); msg.WriteString(SenderName); msg.WriteBoolean(SenderClient != null); if (SenderClient != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 302b5b959..4fd2c36a1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Networking { case ConnectionInitialization.ContentPackageOrder: - DateTime timeNow = DateTime.UtcNow; + SerializableDateTime timeNow = SerializableDateTime.UtcNow; structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 25a9ba4aa..8e904d0d8 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.1.0 + 0.21.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 7820a24dc..f0eae6be8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -351,21 +351,6 @@ namespace Barotrauma } } - public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) - { - if (myTeam == otherTeam) { return true; } - return myTeam switch - { - // NPCs are friendly to the same team and the friendly NPCs - CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, - // Friendly NPCs are friendly to both player teams - CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, - _ => true - }; - } - - public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); - public void ReequipUnequipped() { foreach (var item in unequippedItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 378052dc3..bbe7863ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -206,13 +206,19 @@ namespace Barotrauma private set; } = new HashSet(); - public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget); public static bool IsTargetBeingChasedBy(Character target, Character character) => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive); public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); - private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; + private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; + + private bool IsAttackingOwner(Character other) => + PetBehavior != null && PetBehavior.Owner != null && + !other.IsUnconscious && !other.IsArrested && + other.AIController is HumanAIController humanAI && + humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat && + combat.Enemy != null && combat.Enemy == PetBehavior.Owner; private bool reverse; public bool Reverse @@ -355,7 +361,7 @@ namespace Barotrauma { targetingTag = "owner"; } - else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter)) + else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter))) { targetingTag = "hostile"; } @@ -681,19 +687,22 @@ namespace Barotrauma { if (SelectedAiTarget.Entity is Character targetCharacter) { - bool IsValid(Character.Attacker a) + bool ShouldRetaliate(Character.Attacker a) { Character c = a.Character; - if (c.IsDead || c.Removed) { return false; } - if (!Character.IsFriendly(c)) { return true; } - if (!c.IsPlayer) { return false; } - // Only apply the threshold to players - return a.Damage >= selectedTargetingParams.Threshold; + if (c == null || c.IsUnconscious || c.Removed) { return false; } + // Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group. + if (Character.IsSameSpeciesOrGroup(c)) { return false; } + if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; } + if (c.IsPlayer || Character.IsOnFriendlyTeam(c)) + { + return a.Damage >= selectedTargetingParams.Threshold; + } + return true; } - Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; - if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker)) + Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character; + if (attacker?.AiTarget != null) { - // Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group. ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); State = AIState.Attack; @@ -1502,7 +1511,7 @@ namespace Barotrauma { hitTarget = limb.character; } - if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) + if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget)) { return true; } @@ -2316,7 +2325,7 @@ namespace Barotrauma { t = limb.character; } - if (t != null && (t == target || !Character.IsFriendly(t))) + if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t)))) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 3093150d4..919db14e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -310,7 +310,7 @@ namespace Barotrauma UseIndoorSteeringOutside = false; } - if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) + if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) { // Spot enemies while staying outside or inside an enemy ship. // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior @@ -541,7 +541,7 @@ namespace Barotrauma if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } if (Character.CurrentHull == null) { return; } - bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen; + bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1); bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) @@ -566,17 +566,17 @@ namespace Barotrauma gotoObjective.Abandon = true; } } - if (!oxygenLow) + if (!shouldActOnSuffocation) { return; } } // Diving gear - if (oxygenLow || findItemState != FindItemState.OtherItem) + if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem) { bool needsGear = NeedsDivingGear(Character.CurrentHull, out _); - if (!needsGear || oxygenLow) + if (!needsGear || shouldActOnSuffocation) { bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective(); bool shouldKeepTheGearOn = @@ -591,14 +591,14 @@ namespace Barotrauma Character.CurrentHull.IsWetRoom; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait(); - if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) + if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) { shouldKeepTheGearOn = false; // Remove the suit before we pass out removeDivingSuit = true; } bool takeMaskOff = !shouldKeepTheGearOn; - if (!shouldKeepTheGearOn && !oxygenLow) + if (!shouldKeepTheGearOn && !shouldActOnSuffocation) { if (ObjectiveManager.IsCurrentObjective()) { @@ -647,7 +647,7 @@ namespace Barotrauma var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS)) { - if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { divingSuit.Drop(Character); HandleRelocation(divingSuit); @@ -982,7 +982,7 @@ namespace Barotrauma if (target.CurrentHull != hull) { continue; } if (AIObjectiveRescueAll.IsValidTarget(target, Character)) { - if (AddTargets(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective()) + if (AddTargets(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective()) { var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); @@ -1161,7 +1161,7 @@ namespace Barotrauma freezeAI = true; } } - if (attacker == null || attacker.IsDead || attacker.Removed) + if (attacker == null || attacker.IsUnconscious || attacker.Removed) { // Don't react to the damage if there's no attacker. // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. @@ -1199,7 +1199,7 @@ namespace Barotrauma return; } float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); - bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; + bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; if (isAccidental) { if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) @@ -1279,7 +1279,7 @@ namespace Barotrauma if (otherCharacter.Submarine != attacker.Submarine) { continue; } if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } if (otherCharacter.IsPlayer) { continue; } - if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; } + if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; } bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); if (!isWitnessing) @@ -1299,7 +1299,7 @@ namespace Barotrauma AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false) { - if (!(c.AIController is HumanAIController humanAI)) { return AIObjectiveCombat.CombatMode.None; } + if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; } if (!IsFriendly(attacker)) { if (c.Submarine == null) @@ -1327,7 +1327,7 @@ namespace Barotrauma } if (attacker.IsPlayer && c.TeamID == attacker.TeamID) { - if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID) + if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID) { // Bots in the player team never act aggressively in single player when attacked by the player // In multiplayer, they react only to players attacking them or other crew members @@ -1345,11 +1345,11 @@ namespace Barotrauma isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } - if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) + if (isWitnessing && c.CombatAction != null && !c.IsSecurity) { - return Character.CombatAction.WitnessReaction; + return c.CombatAction.WitnessReaction; } - if (attacker.IsPlayer && FindInstigator() is Character instigator) + if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator) { // The guards don't react to player's aggressions when there's an instigator around isAttackerFightingEnemy = true; @@ -1359,11 +1359,11 @@ namespace Barotrauma { if (c.IsSecurity) { - return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None; + return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive; } else { - return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None; + return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; } } else @@ -1567,20 +1567,20 @@ namespace Barotrauma return false; } - public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); + public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank); /// /// Check whether the character has a diving suit in usable condition plus some oxygen. /// - public static bool HasDivingSuit(Character character, float conditionPercentage = 0) - => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true, + public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) + => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true, predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes)); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// - public static bool HasDivingMask(Character character, float conditionPercentage = 0) - => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); + public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) + => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true); private static List matchingItems = new List(); @@ -2045,7 +2045,7 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other); + bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { return false; } if (!me.IsSameSpeciesOrGroup(other)) { return false; } if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index e277590bd..3957e400a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -352,7 +352,7 @@ namespace Barotrauma Weapon = null; continue; } - if (WeaponComponent.IsLoaded(character)) + if (WeaponComponent.IsNotEmpty(character)) { // All good, the weapon is loaded break; @@ -470,7 +470,7 @@ namespace Barotrauma // Not in the inventory anymore or cannot find the weapon component return false; } - if (!WeaponComponent.IsLoaded(character)) + if (!WeaponComponent.IsNotEmpty(character)) { // Try reloading (and seek ammo) if (!Reload(seekAmmo)) @@ -541,7 +541,7 @@ namespace Barotrauma priority /= 2; } } - if (!weapon.IsLoaded(character)) + if (!weapon.IsNotEmpty(character)) { if (weapon is RangedWeapon && !isAllowedToSeekWeapons) { @@ -554,7 +554,15 @@ namespace Barotrauma priority /= 2; } } - if (Enemy.IsKnockedDown) + + if (Enemy.Params.Health.StunImmunity) + { + if (weapon.Item.HasTag("stunner")) + { + priority /= 2; + } + } + else if (Enemy.IsKnockedDown) { // Enemy is stunned, reduce the priority of stunner weapons. Attack attack = GetAttackDefinition(weapon); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 765b8de83..08d7ea70c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -244,7 +244,8 @@ namespace Barotrauma public bool IsInTargetSlot(Item item) { - if (container?.Inventory is ItemInventory inventory && TargetSlot is not null) + if (TargetSlot == null) { return true; } + if (container?.Inventory is ItemInventory inventory) { return inventory.IsInSlot(item, (int)TargetSlot); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 26f10fd92..e299a8edb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -63,15 +63,16 @@ namespace Barotrauma } else { - if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && + if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || + (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && (needsSuit ? !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) : - !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))) + !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))) { Priority = 100; } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && - character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) + character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) { // Ordered to follow, hold position, or return back to main sub inside a hostile sub // -> ignore find safety unless we need to find a diving gear @@ -137,12 +138,14 @@ namespace Barotrauma private float retryTimer; protected override void Act(float deltaTime) { + if (resetPriority) { return; } var currentHull = character.CurrentHull; + bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; - if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull)) + if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); - bool needsEquipment = false; + bool needsEquipment = shouldActOnSuffocation; if (needsDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 3e5531d20..9604579dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -178,7 +178,7 @@ namespace Barotrauma requiredCondition = () => Leak.Submarine == character.Submarine && Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), - endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull), + endNodeFilter = IsSuitableEndNode, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) SpeakCannotReachCondition = () => !CheckObjectiveSpecific() }, @@ -197,6 +197,14 @@ namespace Barotrauma } }, onCompleted: () => RemoveSubObjective(ref gotoObjective)); + + bool IsSuitableEndNode(PathNode n) + { + if (n.Waypoint.CurrentHull is null) { return false; } + if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; } + // Accept also nodes located in the linked hulls (multi-hull rooms) + return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 407b65230..b3cdaed85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -54,7 +54,7 @@ namespace Barotrauma public bool AllowVariants { get; set; } public bool Equip { get; set; } public bool Wear { get; set; } - public bool RequireLoaded { get; set; } + public bool RequireNonEmpty { get; set; } public bool EvaluateCombatPriority { get; set; } public bool CheckPathForEachItem { get; set; } public bool SpeakIfFails { get; set; } @@ -391,10 +391,10 @@ namespace Barotrauma { if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } } - float itemPriority = 1; + float itemPriority = item.Prefab.BotPriority; if (GetItemPriority != null) { - itemPriority = GetItemPriority(item); + itemPriority *= GetItemPriority(item); } Entity rootInventoryOwner = item.GetRootInventoryOwner(); if (rootInventoryOwner is Item ownerItem) @@ -513,7 +513,7 @@ namespace Barotrauma float lowestCost = float.MaxValue; foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { - if (!(prefab is ItemPrefab itemPrefab)) { continue; } + if (prefab is not ItemPrefab itemPrefab) { continue; } if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id))) { float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ? @@ -561,7 +561,7 @@ namespace Barotrauma if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } - if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } + if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; } return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index e6d81bd12..5355767d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -21,7 +21,7 @@ namespace Barotrauma public bool CheckInventory { get; set; } public bool EvaluateCombatPriority { get; set; } public bool CheckPathForEachItem { get; set; } - public bool RequireLoaded { get; set; } + public bool RequireNonEmpty { get; set; } public bool RequireAllItems { get; set; } private readonly ImmutableArray gearTags; @@ -61,7 +61,7 @@ namespace Barotrauma AllowStealing = AllowStealing, ignoredIdentifiersOrTags = ignoredTags, CheckPathForEachItem = CheckPathForEachItem, - RequireLoaded = RequireLoaded, + RequireNonEmpty = RequireNonEmpty, ItemCount = count, SpeakIfFails = RequireAllItems }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 652ce82a5..142a57783 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -364,8 +364,7 @@ namespace Barotrauma CurrentOrders.RemoveAt(i); continue; } - var currentOrderInfo = character.GetCurrentOrder(currentOrder); - if (currentOrderInfo is Order) + if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo) { int currentPriority = currentOrderInfo.ManualPriority; if (currentOrder.ManualPriority != currentPriority) @@ -539,7 +538,8 @@ namespace Barotrauma KeepActiveWhenReady = true, CheckInventory = true, Equip = false, - FindAllItems = true + FindAllItems = true, + RequireNonEmpty = false }; break; case "findweapon": @@ -555,7 +555,8 @@ namespace Barotrauma KeepActiveWhenReady = false, CheckInventory = false, EvaluateCombatPriority = true, - FindAllItems = false + FindAllItems = false, + RequireNonEmpty = true }; } prepareObjective.KeepActiveWhenReady = false; @@ -600,9 +601,9 @@ namespace Barotrauma Order dismissOrder = currentOrder.GetDismissal(); #if CLIENT - if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) + if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer) { - GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder); + character.SetOrder(dismissOrder, isNewOrder: true, speak: false); } #else GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index d83f75768..de55f4035 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -27,6 +27,7 @@ namespace Barotrauma public bool FindAllItems { get; set; } public bool Equip { get; set; } public bool EvaluateCombatPriority { get; set; } + public bool RequireNonEmpty { get; set; } private AIObjective GetSubObjective() { @@ -74,7 +75,7 @@ namespace Barotrauma Abandon = true; } - else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character)))) + else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character)))) { Reset(); } @@ -106,7 +107,7 @@ namespace Barotrauma CheckInventory = CheckInventory, Equip = Equip, EvaluateCombatPriority = EvaluateCombatPriority, - RequireLoaded = true, + RequireNonEmpty = RequireNonEmpty, RequireAllItems = requireAll }, onCompleted: () => @@ -157,7 +158,7 @@ namespace Barotrauma { EvaluateCombatPriority = EvaluateCombatPriority, SpeakIfFails = true, - RequireLoaded = true + RequireNonEmpty = RequireNonEmpty }; } if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 3df33e367..b75a3152e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -320,10 +320,10 @@ namespace Barotrauma foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { if (treatmentSuitability.Value <= cprSuitability) { continue; } - if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab) + if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab)) { - if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; } - suitableItemIdentifiers.Add(treatmentSuitability.Key); + if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; } + suitableItemIdentifiers.Add(itemPrefab.Identifier); //only list the first 4 items if (itemNameList.Count < 4) { @@ -482,18 +482,6 @@ namespace Barotrauma public static IEnumerable GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs); - public static IEnumerable GetTreatableAfflictions(Character character) - { - var allAfflictions = character.CharacterHealth.GetAllAfflictions(); - foreach (Affliction affliction in allAfflictions) - { - if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } - if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; } - if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } - yield return affliction; - } - } - public override void Reset() { base.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index e9cd9e4bb..20be4494f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -26,7 +26,7 @@ namespace Barotrauma // When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored. // If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players. // On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled. - return character == target || manager.HasOrder() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold; + return character == target || manager.HasOrder() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold; } } @@ -67,15 +67,34 @@ namespace Barotrauma float vitality = 100; vitality -= character.Bleeding * 2; vitality += Math.Min(character.Oxygen, 0); - vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); - foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character)) + foreach (Affliction affliction in GetTreatableAfflictions(character)) { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; + if (affliction.Prefab.AfflictionType == "paralysis") + { + vitality -= affliction.Strength; + } + else if (affliction.Prefab.AfflictionType == "poison") + { + vitality -= affliction.Strength; + } } return Math.Clamp(vitality, 0, 100); } + public static IEnumerable GetTreatableAfflictions(Character character) + { + var allAfflictions = character.CharacterHealth.GetAllAfflictions(); + foreach (Affliction affliction in allAfflictions) + { + if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } + if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; } + if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } + yield return affliction; + } + } + protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 0b1d4cafd..bb4f57c72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -454,7 +454,7 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - IsHanging = false; + IsHanging = IsHanging && character.IsRagdolled; } void UpdateStanding() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 5dc7678ae..5ee296c48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -489,7 +489,7 @@ namespace Barotrauma LocalizedString displayName = Params.DisplayName; if (displayName.IsNullOrWhiteSpace()) { - if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride)) + if (Params.SpeciesTranslationOverride.IsEmpty) { displayName = TextManager.Get($"Character.{SpeciesName}"); } @@ -752,7 +752,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength); } } @@ -822,6 +822,7 @@ namespace Barotrauma public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float EmpVulnerability => Params.Health.EmpVulnerability; + public float PoisonVulnerability => Params.Health.PoisonVulnerability; public float Bloodloss { @@ -1040,6 +1041,8 @@ namespace Barotrauma public bool InWater => AnimController is AnimController { InWater: true }; + public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold; + public bool GodMode = false; public CampaignMode.InteractionType CampaignInteractionType; @@ -2871,6 +2874,23 @@ namespace Barotrauma } else { +#if CLIENT + if (Controlled == this) + { + HealingCooldown.PutOnCooldown(); + } +#elif SERVER + if (GameMain.Server?.ConnectedClients is { } clients) + { + foreach (Client c in clients) + { + if (c.Character != this) { continue; } + + HealingCooldown.SetCooldown(c); + break; + } + } +#endif SelectCharacter(FocusedCharacter); #if CLIENT if (Controlled == this) @@ -3791,9 +3811,10 @@ namespace Barotrauma message.SendDelay -= deltaTime; if (message.SendDelay > 0.0f) { continue; } + bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio); if (message.MessageType == null) { - message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default; + message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default; } #if CLIENT if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) @@ -3803,6 +3824,11 @@ namespace Barotrauma { GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this); } + if (canUseRadio) + { + Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item); + radio.TransmitSignal(s, sentFromChat: true); + } } #endif #if SERVER @@ -4122,13 +4148,13 @@ namespace Barotrauma OnAttackedProjSpecific(attacker, attackResult, stun); if (!wasDead) { - TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality); + TryAdjustAttackerSkill(attacker, attackResult); } - }; + } if (attackResult.Damage > 0) { LastDamage = attackResult; - if (attacker != null) + if (attacker != null && attacker != this && !attacker.Removed) { AddAttacker(attacker, attackResult.Damage); AddEncounter(attacker); @@ -4143,26 +4169,84 @@ namespace Barotrauma partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun); - public void TryAdjustAttackerSkill(Character attacker, float healthChange) + public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult) { if (attacker == null) { return; } - + if (!attacker.IsOnPlayerTeam) { return; } bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID; - if (isEnemy) + if (!isEnemy) { return; } + float weaponDamage = 0; + float medicalDamage = 0; + foreach (var affliction in attackResult.Afflictions) { - if (healthChange < 0.0f) + if (affliction.Prefab.IsBuff) { continue; } + if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } + if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") { - float attackerSkillLevel = attacker.GetSkillLevel("weapons"); - attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(), - -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f)); + if (!Params.Health.PoisonImmunity) + { + float relativeVitality = MaxVitality / 100f; + // Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied. + float dmg = affliction.Strength; + if (relativeVitality > 0) + { + dmg /= relativeVitality; + } + if (PoisonVulnerability > 0) + { + dmg /= PoisonVulnerability; + } + float strength = MaxVitality; + if (Params.AI != null) + { + strength = Params.AI.CombatStrength; + } + // Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x. + float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength)); + dmg *= vitalityFactor; + medicalDamage += dmg * affliction.Prefab.MedicalSkillGain; + } } + else + { + medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain; + } + weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain; } - else if (healthChange > 0.0f) + if (medicalDamage > 0) { - float attackerSkillLevel = attacker.GetSkillLevel("medical"); - attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(), - healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f)); + IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage); } + if (weaponDamage > 0) + { + IncreaseSkillLevel("weapons".ToIdentifier(), 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)); + } + } + + public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null) + { + if (healer == null) { return; } + bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID; + if (isEnemy) { return; } + float medicalGain = healthChange; + if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines)) + { + 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)); } /// @@ -5240,7 +5324,24 @@ namespace Barotrauma public bool IsFriendly(Character other) => IsFriendly(this, other); - public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other); + public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other); + + public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) + { + if (myTeam == otherTeam) { return true; } + return myTeam switch + { + // NPCs are friendly to the same team and the friendly NPCs + CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, + // Friendly NPCs are friendly to both player teams + CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, + _ => true + }; + } + + public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); + public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID); + public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam); public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 81b6598ac..997d13a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -321,6 +321,7 @@ namespace Barotrauma { public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; + public readonly float MinStrength, MaxStrength; public PeriodicEffect(ContentXElement element, string parentDebugName) { @@ -335,8 +336,10 @@ namespace Barotrauma } else { - MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f); - MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval); + MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f); + MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval); + MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f); + MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength); } } } @@ -415,8 +418,8 @@ namespace Barotrauma //how much karma changes when a player applies this affliction to someone (per strength of the affliction) public float KarmaChangeOnApplied; - public float BurnOverlayAlpha; - public float DamageOverlayAlpha; + public readonly float BurnOverlayAlpha; + public readonly float DamageOverlayAlpha; //steam achievement given when the affliction is removed from the controlled character public readonly Identifier AchievementOnRemoved; @@ -427,6 +430,20 @@ namespace Barotrauma public readonly Sprite AfflictionOverlay; public readonly bool AfflictionOverlayAlphaIsLinear; + public readonly bool DamageParticles; + + /// + /// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target. + /// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile. + /// If the affliction is of type buff, the skill is increased only when the target is friendly. + /// + public readonly float MedicalSkillGain; + /// + /// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target. + /// The skill is increased only when the target is hostile. + /// + public readonly float WeaponsSkillGain; + private readonly List effects = new List(); private readonly List periodicEffects = new List(); @@ -528,6 +545,10 @@ namespace Barotrauma ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); + DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true); + WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f); + MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f); + List descriptions = new List(); foreach (var subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 4aeeb1ecb..dcfea21dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -708,7 +708,7 @@ namespace Barotrauma return; } } - if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } + if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; } if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 9771ec56e..79d2eadc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -490,13 +490,9 @@ namespace Barotrauma public int RefJointIndex => Params.RefJoint; - private List wearingItems; - public List WearingItems - { - get { return wearingItems; } - } + public readonly List WearingItems = new List(); - public List OtherWearables { get; private set; } = new List(); + public readonly List OtherWearables = new List(); public bool PullJointEnabled { @@ -640,7 +636,6 @@ namespace Barotrauma this.ragdoll = ragdoll; this.character = character; this.Params = limbParams; - wearingItems = new List(); dir = Direction.Right; body = new PhysicsBody(limbParams); type = limbParams.Type; @@ -772,7 +767,7 @@ namespace Barotrauma tempModifiers.Add(damageModifier); } } - foreach (WearableSprite wearable in wearingItems) + foreach (WearableSprite wearable in WearingItems) { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { @@ -791,10 +786,14 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; - if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0) + if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp") { finalDamageModifier *= character.EmpVulnerability; } + if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")) + { + finalDamageModifier *= character.PoisonVulnerability; + } foreach (DamageModifier damageModifier in tempModifiers) { float damageModifierValue = damageModifier.DamageMultiplier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 44018922f..04204dc96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -21,7 +21,7 @@ namespace Barotrauma public Identifier SpeciesName { get; private set; } [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] - public string SpeciesTranslationOverride { get; private set; } + public Identifier SpeciesTranslationOverride { get; private set; } [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] public string DisplayName { get; private set; } @@ -501,6 +501,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool PoisonImmunity { get; set; } + [Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)] + public float PoisonVulnerability { get; set; } + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float EmpVulnerability { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index bf290e9ae..b4b26579b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -42,7 +42,7 @@ namespace Barotrauma public readonly Version GameVersion; public readonly string ModVersion; public Md5Hash Hash { get; private set; } - public readonly Option InstallTime; + public readonly Option InstallTime; public ImmutableArray Files { get; private set; } @@ -73,7 +73,7 @@ namespace Barotrauma Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); if (item is null) { return true; } - return item.Value.LatestUpdateTime <= installTime; + return item.Value.LatestUpdateTime <= installTime.ToUtcValue(); } public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); @@ -106,10 +106,7 @@ namespace Barotrauma GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); - UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0); - InstallTime = installTimeUnix != 0 - ? Option.Some(ToolBox.Epoch.ToDateTime(installTimeUnix)) - : Option.None(); + InstallTime = rootElement.GetAttributeDateTime("installtime"); var fileResults = rootElement.Elements() .Select(e => ContentFile.CreateFromXElement(this, e)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 509ed5547..a6cb260a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1868,6 +1868,7 @@ namespace Barotrauma commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null)); commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true)); commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true)); + commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true)); InitProjectSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs index 3182cfae9..ac2beeebb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction TurbineOutputSlider, DeconstructButton, RechargeSpeedSlider, - CPRButton + CPRButton, + CloseButton, + MessageBoxCloseButton } [Serialize(ElementId.None, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index adb68793d..1a741f3b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -21,7 +21,7 @@ namespace Barotrauma for (int i = 0; i < Submarine.MainSubs.Length; i++) { var sub = Submarine.MainSubs[i]; - if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } + if (sub == null || sub.Info.InitialSuppliesSpawned || sub.Info.IsManuallyOutfitted || !sub.Info.IsPlayer) { continue; } //1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs) SpawnStartItems(sub, startItemSet); //2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 06d74e3fc..ddfcd55e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -5,6 +5,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -13,13 +14,11 @@ namespace Barotrauma abstract partial class CampaignMode : GameMode { [NetworkSerialize] - public struct SaveInfo : INetSerializableStruct - { - public string FilePath; - public int SaveTime; - public string SubmarineName; - public string[] EnabledContentPackageNames; - } + public readonly record struct SaveInfo( + string FilePath, + Option SaveTime, + string SubmarineName, + ImmutableArray EnabledContentPackageNames) : INetSerializableStruct; public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; @@ -1114,7 +1113,6 @@ namespace Barotrauma if (item.Components.None(c => c is Pickable)) { continue; } if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; } if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } - if (item.Container?.GetComponent() is { DrawInventory: false }) { continue; } itemsToTransfer.Add((item, item.Container)); item.Submarine = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 5933cf203..d902e245a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -546,9 +546,7 @@ namespace Barotrauma StatusEffect.StopAll(); #if CLIENT -#if !DEBUG - GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; -#endif + GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode; if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } #endif @@ -1074,7 +1072,10 @@ namespace Barotrauma XDocument doc = new XDocument(new XElement("Gamesession")); XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); - rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); + rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime())); + #warning TODO: after this gets on main, replace savetime with the commented line + //rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow)); + rootElement.Add(new XAttribute("version", GameMain.Version)); if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index c96413662..5c416862d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -692,11 +692,13 @@ namespace Barotrauma /// Gets the progress that is shown on the store interface. /// Includes values stored in the metadata and , and takes submarine tier and class restrictions into account /// - public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category) + /// Submarine used to determine the upgrade limit. If not defined, will default to the current sub. + public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info = null) { if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); } - return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), prefab.GetMaxLevelForCurrentSub()); + int maxLevel = info is null ? prefab.GetMaxLevelForCurrentSub() : prefab.GetMaxLevel(info); + return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), maxLevel); int GetPendingLevel() { @@ -713,6 +715,14 @@ namespace Barotrauma return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0); } + /// + /// Gets the level of the upgrade that is stored in the metadata. Takes into account the limits of the provided submarine. + /// + public int GetRealUpgradeLevelForSub(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo info) + { + return Math.Min(GetRealUpgradeLevel(prefab, category), prefab.GetMaxLevel(info)); + } + /// /// Stores the target upgrade level in the campaign metadata. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a9eabf2c6..f627284a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; @@ -308,6 +309,8 @@ namespace Barotrauma #endif } + if (item.GetComponent() == null || item.AllowedSlots.None()) { return false; } + bool inSuitableSlot = false; bool inWrongSlot = false; int currentSlot = -1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index bbf59eacf..3ef615e8a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -289,7 +289,16 @@ namespace Barotrauma.Items.Components return; } - if (!(joint is WeldJoint)) + if (joint == null) + { + string errorMsg = "Error while locking a docking port (joint between submarines doesn't exist)." + + " Submarine: " + (item.Submarine?.Info.Name ?? "null") + + ", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null"); + GameAnalyticsManager.AddErrorEventOnce("DockingPort.Lock:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return; + } + + if (joint is not WeldJoint) { DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index cf33a8eff..96d89ddc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -1,12 +1,9 @@ using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; #if CLIENT using Barotrauma.Lights; #endif @@ -206,6 +203,8 @@ namespace Barotrauma.Items.Components IsHorizontal = element.GetAttributeBool("horizontal", false); canBePicked = element.GetAttributeBool("canbepicked", false); autoOrientGap = element.GetAttributeBool("autoorientgap", false); + + allowedSlots.Clear(); foreach (var subElement in element.Elements()) { @@ -359,7 +358,10 @@ namespace Barotrauma.Items.Components { lastBrokenTime = Timing.TotalTime; //the door has to be restored to 50% health before collision detection on the body is re-enabled - if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f && + + //multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_ + //otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider + if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { IsBroken = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index c2b5630bc..b59784eeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -58,9 +58,6 @@ namespace Barotrauma.Items.Components set; } - //the angle in which the Character holds the item - protected float holdAngle; - public PhysicsBody Body { get { return item.body ?? body; } @@ -143,6 +140,7 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } + protected float holdAngle; #if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] #else @@ -154,6 +152,18 @@ namespace Barotrauma.Items.Components set { holdAngle = MathHelper.ToRadians(value); } } + protected float aimAngle; +#if DEBUG + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item while aiming (in degrees, relative to the rotation of the character's hand).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif + public float AimAngle + { + get { return MathHelper.ToDegrees(aimAngle); } + set { aimAngle = MathHelper.ToRadians(value); } + } + private Vector2 swingAmount; #if DEBUG [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] @@ -552,6 +562,7 @@ namespace Barotrauma.Items.Components { return false; } + bool wasAttached = IsAttached; if (base.OnPicked(picker)) { DeattachFromWall(); @@ -560,7 +571,7 @@ namespace Barotrauma.Items.Components if (GameMain.Server != null && attachable) { item.CreateServerEvent(this); - if (picker != null) + if (picker != null && wasAttached) { GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction); } @@ -688,16 +699,22 @@ namespace Barotrauma.Items.Components if (maxAttachableCount == 0) { #if CLIENT - GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red); + if (character == Character.Controlled) + { + GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red); + } #endif return false; } else if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT - GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); + if (character == Character.Controlled) + { + GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); + } #endif - return false; + return false; } } @@ -875,9 +892,13 @@ namespace Barotrauma.Items.Components scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); - if (!aim) + if (aim) { + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle); + } + else + { + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); var rope = GetRope(); if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index cfb71aca1..548147bfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components { UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true); if (ac.InWater) { ac.LockFlipping(); @@ -472,8 +472,8 @@ namespace Barotrauma.Items.Components } if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity)); serverLogger ??= new System.Text.StringBuilder(); serverLogger.Clear(); serverLogger.Append($"{picker?.LogName} used {item.Name}"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 95c40bb0f..9f83723a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components get { return allowedSlots; } } + public bool PickingDone => pickTimer >= PickingTime; + public Character Picker { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index cd81939f5..8b8ff6601 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -205,12 +205,12 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower)); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, targetCharacter: CurrentThrower)); } if (!(GameMain.NetworkMember is { IsClient: true })) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" - ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower); + ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: CurrentThrower, user: CurrentThrower); } throwState = ThrowState.None; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 1643336eb..9d8b2e08e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -125,8 +125,8 @@ namespace Barotrauma.Items.Components get { return drawable; } set { - if (value == drawable) return; - if (!(this is IDrawableComponent)) + if (value == drawable) { return; } + if (this is not IDrawableComponent) { DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)"); return; @@ -236,10 +236,7 @@ namespace Barotrauma.Items.Components set; } - /// - /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). - /// - [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] + [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")] public float CombatPriority { get; private set; } /// @@ -697,7 +694,7 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } - public bool IsLoaded(Character user, bool checkContainedItems = true) => + public bool IsNotEmpty(Character user, bool checkContainedItems = true) => HasRequiredContainedItems(user, addMessage: false) && (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index f91a7089d..68c59aaee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -114,7 +114,7 @@ namespace Barotrauma.Items.Components [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")] public bool DrawInventory { get; @@ -142,6 +142,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No)] + public bool AllowAccess { get; set; } + [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } @@ -534,12 +537,12 @@ namespace Barotrauma.Items.Components public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } public override bool Select(Character character) { - if (!DrawInventory) { return false; } + if (!AllowAccess) { return false; } if (item.Container != null) { return false; } if (AccessOnlyWhenBroken) { @@ -575,7 +578,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - if (!DrawInventory) { return false; } + if (!AllowAccess) { return false; } if (AccessOnlyWhenBroken) { if (item.Condition > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 08897d67a..23c9487a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -112,27 +112,34 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")] public bool StickToCharacters { get; set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")] public bool StickToStructures { get; set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")] public bool StickToItems { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")] + public bool StickToDoors + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")] public bool StickToDeflective { @@ -457,36 +464,36 @@ namespace Barotrauma.Items.Components Vector2 rayEndWorld = rayStartWorld + dir * worldDist; List hits = new List(); - hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine)); if (item.Submarine != null) { //shooting indoors, do a hitscan outside as well hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null)); - //also in the coordinate space of docked subs - foreach (Submarine dockedSub in item.Submarine.DockedTo) - { - if (dockedSub == item.Submarine) { continue; } - hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub)); - } + //do a hitscan in other subs' coordinate spaces + RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition); } else + { + RayCastInOtherSubs(rayStart, rayEnd); + } + + void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd) { //shooting outdoors, see if we can hit anything inside a sub foreach (Submarine submarine in Submarine.Loaded) { + if (submarine == item.Submarine) { continue; } var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine); //transform back to world coordinates for (int i = 0; i < inSubHits.Count; i++) { inSubHits[i] = new HitscanResult( - inSubHits[i].Fixture, - inSubHits[i].Point + submarine.SimPosition, - inSubHits[i].Normal, + inSubHits[i].Fixture, + inSubHits[i].Point + submarine.SimPosition, + inSubHits[i].Normal, inSubHits[i].Fraction); } - hits.AddRange(inSubHits); } } @@ -993,8 +1000,8 @@ namespace Barotrauma.Items.Components } if (GameMain.NetworkMember is { IsServer: true } server) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition)); } } else @@ -1003,8 +1010,8 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User); if (GameMain.NetworkMember is { IsServer: true } server) { - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); - server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition)); } } } @@ -1012,13 +1019,12 @@ namespace Barotrauma.Items.Components target.Body.ApplyLinearImpulse(velocity * item.body.Mass); target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f); - if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) + if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) { DisableProjectileCollisions(); } - if (attackResult.AppliedDamageModifiers != null && - (attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)) + if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective) { item.body.LinearVelocity *= deflectedSpeedMultiplier; } @@ -1028,7 +1034,7 @@ namespace Barotrauma.Items.Components ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || - (StickToItems && target.Body.UserData is Item)))) + (target.Body.UserData is Item i && (i.GetComponent() != null ? StickToDoors : StickToItems))))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index bb38957ea..d15979d30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -302,33 +302,16 @@ namespace Barotrauma.Items.Components var sourceBody = GetBodyToPull(source); if (sourceBody != null) { - var targetBody = GetBodyToPull(target); - if (targetBody != null && targetBody.UserData is not Character) + if (user != null && user.InWater) { - sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); - } - float forceMultiplier = 1; - if (user != null) - { - user.AnimController.Hang(); - if (user.InWater) + if (user.IsRagdolled) { - if (user.IsRagdolled) - { - forceMultiplier = 0; - } - } - else - { - forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f; - // Prevents too easy smashing to the walls - forceDir.X /= 4; - // Prevents rubberbanding up and down - if (forceDir.Y < 0) - { - forceDir.Y = 0; - } + // Reel in towards the target. + user.AnimController.Hang(); + float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce; + sourceBody.ApplyForce(forceDir * force); } + // Take the target velocity into account. if (targetCharacter != null) { var myCollider = user.AnimController.Collider; @@ -341,9 +324,15 @@ namespace Barotrauma.Items.Components } } } + else + { + var targetBody = GetBodyToPull(target); + if (targetBody != null) + { + sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); + } + } } - float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier; - sourceBody.ApplyForce(forceDir * force); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 6d45bad3c..da887ef93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -187,6 +187,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] + public bool AlphaBlend + { + get; + set; + } + public float TemporaryFlickerTimer; public override void Move(Vector2 amount, bool ignoreContacts = false) @@ -241,6 +248,7 @@ namespace Barotrauma.Items.Components SetLightSourceState(IsActive); turret = item.GetComponent(); #if CLIENT + Drawable = AlphaBlend && Light.LightSprite != null; if (Screen.Selected.IsEditor) { OnMapLoaded(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 1240fb9cb..f596fd3d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components private string prevSignal; - private readonly int[] channelMemory = new int[ChannelMemorySize]; + private int[] channelMemory = new int[ChannelMemorySize]; private Connection signalInConnection; private Connection signalOutConnection; @@ -94,7 +94,17 @@ namespace Barotrauma.Items.Components { list.Add(this); IsActive = true; - channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + if (channelMemory.Length != ChannelMemorySize) + { + DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing..."); + Array.Resize(ref channelMemory, ChannelMemorySize); + } } public override void OnItemLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 67084c123..bd06a6ed3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -44,7 +44,16 @@ namespace Barotrauma } public LimbType Limb { get; private set; } public bool HideLimb { get; private set; } - public bool HideOtherWearables { get; private set; } + + public enum ObscuringMode + { + None, + Hide, + AlphaClip + } + public ObscuringMode ObscureOtherWearables { get; private set; } + public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide; + public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip; public bool CanBeHiddenByOtherWearables { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } @@ -130,7 +139,7 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - HideOtherWearables = false; + ObscureOtherWearables = ObscuringMode.None; InheritLimbDepth = true; InheritScale = true; InheritOrigin = true; @@ -202,7 +211,16 @@ namespace Barotrauma Sprite = new Sprite(SourceElement, file: SpritePath); Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true); HideLimb = SourceElement.GetAttributeBool("hidelimb", false); - HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false); + + foreach (var mode in Enum.GetValues()) + { + if (mode == ObscuringMode.None) { continue; } + if (SourceElement.GetAttributeBool($"{mode}OtherWearables", false)) + { + ObscureOtherWearables = mode; + } + } + CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); var scale = SourceElement.GetAttribute("inheritscale"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 447b99409..c31f6c337 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -585,6 +585,8 @@ namespace Barotrauma item.body.Enabled = false; item.body.BodyType = FarseerPhysics.BodyType.Dynamic; item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false); + //update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies) + item.body.Update(); } #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 628a81168..346f772dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -897,7 +897,7 @@ namespace Barotrauma defaultRect = newRect; rect = newRect; - condition = MaxCondition = Prefab.Health; + condition = MaxCondition = prevCondition = Prefab.Health; ConditionPercentage = 100.0f; lastSentCondition = condition; @@ -999,13 +999,6 @@ namespace Barotrauma if (ic == null) break; AddComponent(ic); - - if (ic is IDrawableComponent && ic.Drawable) - { - drawableComponents.Add(ic as IDrawableComponent); - hasComponentsToDraw = true; - } - if (ic is Repairable) repairables.Add((Repairable)ic); break; } } @@ -1020,6 +1013,14 @@ namespace Barotrauma } } + if (ic is Repairable repairable) { repairables.Add(repairable); } + + if (ic is IDrawableComponent && ic.Drawable) + { + drawableComponents.Add(ic as IDrawableComponent); + hasComponentsToDraw = true; + } + if (ic.statusEffectLists == null) { continue; } if (ic.InheritStatusEffects) { @@ -1751,6 +1752,7 @@ namespace Barotrauma RecalculateConditionValues(); + bool wasPreviousConditionChanged = false; if (condition == 0.0f && prevCondition > 0.0f) { //Flag connections to be updated as device is broken @@ -1763,6 +1765,8 @@ namespace Barotrauma } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif + // Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow. + SetPreviousCondition(); ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } else if (condition > 0.0f && prevCondition <= 0.0f) @@ -1793,9 +1797,18 @@ namespace Barotrauma } } - LastConditionChange = condition - prevCondition; - ConditionLastUpdated = Timing.TotalTime; - prevCondition = condition; + if (!wasPreviousConditionChanged) + { + SetPreviousCondition(); + } + + void SetPreviousCondition() + { + LastConditionChange = condition - prevCondition; + ConditionLastUpdated = Timing.TotalTime; + prevCondition = condition; + wasPreviousConditionChanged = true; + } static void flagChangedConnections(Dictionary connections) { @@ -2696,7 +2709,7 @@ namespace Barotrauma return; } - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2713,7 +2726,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character); + ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character); if (ic.DeleteOnUse) { remove = true; } } @@ -2727,7 +2740,7 @@ namespace Barotrauma public void SecondaryUse(float deltaTime, Character character = null) { - if (condition == 0.0f) { return; } + if (condition <= 0.0f) { return; } bool remove = false; @@ -2763,6 +2776,13 @@ namespace Barotrauma if (!UseInHealthInterface) { return; } #if CLIENT + if (user == Character.Controlled) + { + if (HealingCooldown.IsOnCooldown) { return; } + + HealingCooldown.PutOnCooldown(); + } + if (GameMain.Client != null) { GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb)); @@ -2783,13 +2803,13 @@ namespace Barotrauma #endif ic.WasUsed = true; - ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user); - ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user); + ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: user); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: character, user: user); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb, useTarget: character)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb, useTarget: character)); } if (ic.DeleteOnUse) { remove = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index db4471080..761e84d24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -769,6 +769,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.No)] public bool ShowHealthBar { get; private set; } + [Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")] + public float BotPriority { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -938,14 +941,20 @@ namespace Barotrauma AllowDeconstruct = true; RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); - foreach (XElement deconstructItem in subElement.Elements()) + foreach (XElement itemElement in subElement.Elements()) { - if (deconstructItem.Attribute("name") != null) + if (itemElement.Attribute("name") != null) { DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - deconstructItems.Add(new DeconstructItem(deconstructItem, Identifier)); + var deconstructItem = new DeconstructItem(itemElement, Identifier); + if (deconstructItem.ItemIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier."); + continue; + } + deconstructItems.Add(deconstructItem); } RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index beace2553..92effd495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -18,9 +18,9 @@ namespace Barotrauma public const ushort ReservedIDStart = ushort.MaxValue - 3; - public const ushort MaxEntityCount = ushort.MaxValue - 2; //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values + public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values - private static Dictionary dictionary = new Dictionary(); + private static readonly Dictionary dictionary = new Dictionary(); public static IReadOnlyCollection GetEntities() { return dictionary.Values; @@ -85,6 +85,28 @@ namespace Barotrauma this.Submarine = submarine; spawnTime = Timing.TotalTime; + if (dictionary.Count >= MaxEntityCount) + { + Dictionary entityCounts = new Dictionary(); + foreach (var entity in dictionary) + { + if (entity.Value is MapEntity me) + { + if (entityCounts.ContainsKey(me.Prefab.Identifier)) + { + entityCounts[me.Prefab.Identifier]++; + } + else + { + entityCounts[me.Prefab.Identifier] = 1; + } + } + } + string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " + + string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}")); + throw new Exception(errorMsg); + } + //give a unique ID ID = DetermineID(id, submarine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index ab5c17c8b..f37d520d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -50,6 +50,12 @@ namespace Barotrauma Description = TextManager.Get($"EntityDescription.{Identifier}"); Tags = Enumerable.Empty().ToImmutableHashSet(); + string description = element.GetAttributeString("description", string.Empty); + if (!description.IsNullOrEmpty()) + { + Description = Description.Fallback(description); + } + List containedItemIDs = new List(); foreach (XElement entityElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 1ac0bfd73..f926af7ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -112,10 +112,9 @@ namespace Barotrauma (int)MathUtils.Round(generationParams.Height, Level.GridCellSize)); } - public LevelData(XElement element, float? forceDifficulty = null) + public LevelData(XElement element, float? forceDifficulty = null, bool clampDifficultyToBiome = false) { Seed = element.GetAttributeString("seed", ""); - Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f); Size = element.GetAttributePoint("size", new Point(1000)); Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type); @@ -131,10 +130,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\"."); GenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(l => l.Type == Type); - if (GenerationParams == null) - { - GenerationParams = LevelGenerationParams.LevelParams.First(); - } + GenerationParams ??= LevelGenerationParams.LevelParams.First(); } InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin); @@ -147,10 +143,16 @@ namespace Barotrauma Biome = Biome.Prefabs.First(); } - string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { }); + Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f); + if (clampDifficultyToBiome) + { + Difficulty = MathHelper.Clamp(Difficulty, Biome.MinDifficulty, Biome.AdjustedMaxDifficulty); + } + + string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty()); EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); - string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); + string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 19224427b..7fa9b6fc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -559,12 +559,9 @@ namespace Barotrauma killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty()).ToHashSet(); System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!"); - if (Type == null) - { - Type = LocationType.Prefabs.First(); - } + Type ??= LocationType.Prefabs.First(); - LevelData = new LevelData(element.Element("Level")); + LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true); PortraitId = ToolBox.StringToInt(Name); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index c40ed58c8..a5c3ffac7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -531,11 +531,15 @@ namespace Barotrauma /// /// Calculated from . Can be used when the sub hasn't been loaded and we can't access . /// - public float GetRealWorldCrushDepth() + public bool IsCrushDepthDefinedInStructures(out float realWorldCrushDepth) { - if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; } + if (SubmarineElement == null) + { + realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; + return false; + } bool structureCrushDepthsDefined = false; - float realWorldCrushDepth = float.PositiveInfinity; + realWorldCrushDepth = float.PositiveInfinity; foreach (var structureElement in SubmarineElement.GetChildElements("structure")) { string name = structureElement.Attribute("name")?.Value ?? ""; @@ -553,7 +557,7 @@ namespace Barotrauma { realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; } - return realWorldCrushDepth; + return structureCrushDepthsDefined; } //saving/loading ---------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs index e08ff3ca1..dbb4c9276 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Networking public readonly Either AddressOrAccountId; public readonly string Reason; - public DateTime? ExpirationTime; + public Option ExpirationTime; public readonly UInt32 UniqueIdentifier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index e3f946325..29855637a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -160,7 +160,8 @@ namespace Barotrauma { typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteIdentifier) }, { typeof(AccountId), new ReadWriteBehavior(ReadAccountId, WriteAccountId) }, { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, - { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } + { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }, + { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) } }; private static readonly ImmutableDictionary, Func> BehaviorFactories = new Dictionary, Func> @@ -512,6 +513,41 @@ namespace Barotrauma WriteSingle(y, attribute, msg, bitField); } + private static readonly Range ValidTickRange + = new Range( + start: DateTime.MinValue.Ticks, + end: DateTime.MaxValue.Ticks); + private static readonly Range ValidTimeZoneMinuteRange + = new Range( + start: (Int16)TimeSpan.FromHours(-12).TotalMinutes, + end: (Int16)TimeSpan.FromHours(14).TotalMinutes); + + private static SerializableDateTime ReadSerializableDateTime( + IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) + { + var ticks = inc.ReadInt64(); + var timezone = inc.ReadInt16(); + + if (!ValidTickRange.Contains(ticks)) + { + throw new Exception($"Incoming SerializableDateTime ticks out of range (ticks: {ticks}, timezone: {timezone})"); + } + if (!ValidTimeZoneMinuteRange.Contains(timezone)) + { + throw new Exception($"Incoming SerializableDateTime timezone out of range (ticks: {ticks}, timezone: {timezone})"); + } + + return new SerializableDateTime(new DateTime(ticks), + new SerializableTimeZone(TimeSpan.FromMinutes(timezone))); + } + + private static void WriteSerializableDateTime( + SerializableDateTime dateTime, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) + { + msg.WriteInt64(dateTime.Ticks); + msg.WriteInt16((Int16)(dateTime.TimeZone.Value.Ticks / TimeSpan.TicksPerMinute)); + } + private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue; private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 02fc9f386..c8042a945 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -302,7 +302,7 @@ namespace Barotrauma.Networking public ServerContentPackage() { } - public ServerContentPackage(ContentPackage contentPackage, DateTime referenceTime) + public ServerContentPackage(ContentPackage contentPackage, SerializableDateTime referenceTime) { Name = contentPackage.Name; Hash = contentPackage.Hash; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 8108d7fe8..9e662479a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,15 +1,14 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Xml; using System.Xml.Linq; -using Microsoft.Xna.Framework; using File = Barotrauma.IO.File; using FileStream = Barotrauma.IO.FileStream; using Path = Barotrauma.IO.Path; @@ -315,7 +314,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } @@ -357,7 +356,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; @@ -376,7 +375,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; @@ -395,12 +394,22 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; } + public static Option GetAttributeDateTime( + this XElement element, string name) + { + var attribute = element?.GetAttribute(name); + if (attribute == null) { return Option.None(); } + + string attrVal = attribute.Value; + return SerializableDateTime.Parse(attrVal); + } + public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue) { var attribute = element?.GetAttribute(name); @@ -414,7 +423,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } return val; @@ -439,7 +448,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } @@ -464,7 +473,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } @@ -566,13 +575,26 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e); + LogAttributeError(attribute, element, e); } } return colorValue; } + private static void LogAttributeError(XAttribute attribute, XElement element, Exception e) + { + string elementStr = element.ToString(); + if (elementStr.Length > 500) + { + DebugConsole.ThrowError($"Error when reading attribute \"{attribute}\"!", e); + } + else + { + DebugConsole.ThrowError($"Error when reading attribute \"{attribute.Name}\" from {elementStr}!", e); + } + } + #if CLIENT public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index c891bb02d..9f3d59506 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1455,7 +1455,7 @@ namespace Barotrauma if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); - RegisterTreatmentResults(entity, limb, affliction, result); + RegisterTreatmentResults(user, entity as Item, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } @@ -1467,7 +1467,7 @@ namespace Barotrauma newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); - RegisterTreatmentResults(entity, limb, affliction, result); + RegisterTreatmentResults(user, entity as Item, limb, affliction, result); } } @@ -1498,17 +1498,18 @@ namespace Barotrauma { targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); } - targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality); - if (user != null && user != targetCharacter) + if (!targetCharacter.IsDead) { - if (!targetCharacter.IsDead) + float healthChange = targetCharacter.Vitality - prevVitality; + targetCharacter.AIController?.OnHealed(healer: user, healthChange); + if (user != null) { - targetCharacter.TryAdjustAttackerSkill(user, targetCharacter.Vitality - prevVitality); - } - }; + targetCharacter.TryAdjustHealerSkill(user, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, healthChange, 0.0f); #endif + } + } } } @@ -2073,14 +2074,14 @@ namespace Barotrauma if (character.Removed) { continue; } newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); - element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result); + element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, result.HitLimb, affliction, result); } else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); - element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); + element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result); } } @@ -2111,17 +2112,18 @@ namespace Barotrauma { targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); } - if (element.User != null && element.User != targetCharacter) + if (!targetCharacter.IsDead) { - targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality); - if (!targetCharacter.IsDead) + float healthChange = targetCharacter.Vitality - prevVitality; + targetCharacter.AIController?.OnHealed(healer: element.User, healthChange); + if (element.User != null) { - targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality); - } - }; + targetCharacter.TryAdjustHealerSkill(element.User, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, healthChange, 0.0f); #endif + } + } } } } @@ -2170,7 +2172,7 @@ namespace Barotrauma { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); } - else if (affliction.Prefab.AfflictionType == "poison") + else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } @@ -2183,23 +2185,25 @@ namespace Barotrauma return affliction; } - private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result) + private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affliction affliction, AttackResult result) { - if (entity is Item item && item.UseInHealthInterface && limb != null) + if (item == null) { return; } + if (!item.UseInHealthInterface) { return; } + if (limb == null) { return; } + foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) { - foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) + if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && + (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { - if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && - (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) + if (type == ActionType.OnUse || type == ActionType.OnSuccess) { - if (type == ActionType.OnUse || type == ActionType.OnSuccess) - { - limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; - } - else if (type == ActionType.OnFailure) - { - limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; - } + limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; + limb.character.TryAdjustHealerSkill(user, affliction: affliction); + } + else if (type == ActionType.OnFailure) + { + limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; + limb.character.TryAdjustHealerSkill(user, affliction: affliction); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 071f08337..78cd9451d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -495,7 +495,8 @@ namespace Barotrauma.Steam new XAttribute("corepackage", isCorePackage), new XAttribute("modversion", modVersion), new XAttribute("gameversion", gameVersion), - new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime))); + #warning TODO: stop writing Unix time after this gets on main + new XAttribute("installtime", new SerializableDateTime(updateTime).ToUnixTime())); if ((modPathDirName ?? modName).ToIdentifier() != itemTitle) { root.Add(new XAttribute("altnames", modPathDirName ?? modName)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 55e19dfad..09ebf269f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -302,6 +302,7 @@ namespace Barotrauma UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier()); } + // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest. if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null) { UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); @@ -315,6 +316,7 @@ namespace Barotrauma } else { + // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest. if (item.Prefab.Identifier == "morbusine") { UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index fc64098bb..88731d001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -46,7 +46,7 @@ namespace Barotrauma case int _: case double _: { - var value = (float) OriginalValue; + var value = Convert.ToSingle(OriginalValue); return level == 0 ? value : CalculateUpgrade(value, level, Multiplier); } case bool _ when bool.TryParse(Multiplier, out bool result): diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 6edb6b94f..f7b33b9aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -546,7 +546,7 @@ namespace Barotrauma foreach (Item item in itemsToRemove) { - item.Remove(); + Entity.Spawner.AddItemToRemoveQueue(item); } if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs new file mode 100644 index 000000000..7d6613c33 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CoordinateSpace2D.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; +namespace Barotrauma.Utils; + +public struct CoordinateSpace2D +{ + public static readonly CoordinateSpace2D CanonicalSpace = new CoordinateSpace2D + { + Origin = Vector2.Zero, + I = Vector2.UnitX, + J = Vector2.UnitY + }; + + public Vector2 Origin; + public Vector2 I; + public Vector2 J; + + public Matrix LocalToCanonical + => new Matrix( + m11: I.X, m12: I.Y, m13: 0f, m14: 0f, + m21: J.X, m22: J.Y, m23: 0f, m24: 0f, + m31: 0f, m32: 0f, m33: 1f, m34: 0f, + m41: 0f, m42: 0f, m43: 0f, m44: 1f) + * Matrix.CreateTranslation(Origin.X, Origin.Y, 0f); + + public Matrix CanonicalToLocal => Matrix.Invert(LocalToCanonical); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 872ba4426..8b736ad74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -19,6 +19,9 @@ namespace Barotrauma static class MathUtils { + public static Vector2 DiscardZ(this Vector3 vector) + => new Vector2(vector.X, vector.Y); + public static float Percentage(float portion, float total) { return portion / total * 100; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 55120383d..f4adb8a06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using System.Text.RegularExpressions; using Barotrauma.IO; using Microsoft.Xna.Framework; +using System.Collections.Immutable; namespace Barotrauma { @@ -292,10 +293,11 @@ namespace Barotrauma } if (doc?.Root == null) { - saveInfos.Add(new CampaignMode.SaveInfo() - { - FilePath = file - }); + saveInfos.Add(new CampaignMode.SaveInfo( + FilePath: file, + SaveTime: Option.None, + SubmarineName: "", + EnabledContentPackageNames: ImmutableArray.Empty)); } else { @@ -326,13 +328,11 @@ namespace Barotrauma enabledContentPackageNames.Add(packageName.Replace(@"\|", "|")); } - saveInfos.Add(new CampaignMode.SaveInfo() - { - FilePath = file, - SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""), - SaveTime = doc.Root.GetAttributeInt("savetime", 0), - EnabledContentPackageNames = enabledContentPackageNames.ToArray(), - }); + saveInfos.Add(new CampaignMode.SaveInfo( + FilePath: file, + SaveTime: doc.Root.GetAttributeDateTime("savetime"), + SubmarineName: doc?.Root?.GetAttributeStringUnrestricted("submarine", ""), + EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray())); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs new file mode 100644 index 000000000..9f65e0ef8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs @@ -0,0 +1,266 @@ +#nullable enable +using System; +using System.Globalization; +using System.Linq; + +namespace Barotrauma +{ + public readonly struct SerializableTimeZone + { + /// + /// Diff from UTC + /// + public readonly TimeSpan Value; + + private readonly int hours; + private readonly int minutes; + private readonly char sign; + + public SerializableTimeZone(TimeSpan value) + { + Value = new TimeSpan( + hours: value.Hours, + minutes: value.Minutes, + seconds: 0); + + hours = Math.Abs(value.Hours); + minutes = Math.Abs(value.Minutes); + sign = Value.Ticks < 0 ? '-' : '+'; + } + + public override string ToString() + => (hours, minutes) switch + { + (0, 0) => "UTC", + (_, 0) => $"UTC{sign}{hours}", + (_, < 10) => $"UTC{sign}{hours}:0{minutes}", + _ => $"UTC{sign}{hours}:{minutes}" + }; + + public override int GetHashCode() + => HashCode.Combine(Value.Ticks < 0, hours, minutes); + + public static SerializableTimeZone FromDateTime(DateTime dateTime) + { + if (dateTime.Kind == DateTimeKind.Unspecified) + { + throw new InvalidOperationException( + $"Cannot determine timezone for {nameof(DateTime)} " + + $"of unspecified kind"); + } + var utcDateTime = dateTime.ToUniversalTime(); + return new SerializableTimeZone(dateTime - utcDateTime); + } + + public static SerializableTimeZone LocalTimeZone + => FromDateTime(DateTime.Now); + + public static Option Parse(string str) + { + if (!str.StartsWith("UTC", StringComparison.OrdinalIgnoreCase)) + { + return Option.None(); + } + string timeZoneStr = str[3..]; + bool negative = timeZoneStr.StartsWith("-"); + bool valid = negative || timeZoneStr.StartsWith("+"); + + if (!valid) { return Option.None(); } + + timeZoneStr = str[4..]; + + TimeSpan makeTimeSpan(int hours, int minutes) + => new TimeSpan( + ticks: (hours * TimeSpan.TicksPerHour + minutes * TimeSpan.TicksPerMinute) + * (negative ? -1L : 1L)); + + if (timeZoneStr.IndexOf(':') is var hrMinSeparator && hrMinSeparator > 0) + { + if (int.TryParse(timeZoneStr[..hrMinSeparator], out int timeZoneHours) + && int.TryParse(timeZoneStr[(hrMinSeparator + 1)..], out int timeZoneMinutes)) + { + return Option.Some( + new SerializableTimeZone(makeTimeSpan(timeZoneHours, timeZoneMinutes))); + } + } + else if (int.TryParse(timeZoneStr, out int timeZoneHours)) + { + return Option.Some( + new SerializableTimeZone(makeTimeSpan(timeZoneHours, 0))); + } + return Option.None(); + } + } + + /// + /// DateTime wrapper that tries to offer a reliable + /// string representation that's also human-friendly + /// + public readonly struct SerializableDateTime : IComparable + { + public bool Equals(SerializableDateTime other) + => ToUtc().value.Equals(other.ToUtc().value); + + public override bool Equals(object? obj) + => obj is SerializableDateTime other && Equals(other); + + private static DateTime UnixEpoch(DateTimeKind kind) + => new DateTime(1970, 1, 1, 0, 0, 0, kind); + + private readonly DateTime value; + public readonly SerializableTimeZone TimeZone; + + public SerializableDateTime(DateTime value) : this(value, default) + { + if (value.Kind == DateTimeKind.Unspecified) + { + throw new Exception($"Timezone required when constructing {nameof(SerializableDateTime)} " + + $"from {nameof(DateTime)} of unspecified kind"); + } + TimeZone = SerializableTimeZone.FromDateTime(value); + } + + public SerializableDateTime(DateTime value, SerializableTimeZone timeZone) + { + this.value = new DateTime( + value.Year, value.Month, value.Day, + value.Hour, value.Minute, value.Second, + DateTimeKind.Unspecified); + TimeZone = timeZone; + } + + public static SerializableDateTime LocalNow + => new SerializableDateTime(DateTime.Now); + + public static SerializableDateTime UtcNow + => new SerializableDateTime(DateTime.UtcNow); + + public SerializableDateTime ToUtc() + => new SerializableDateTime( + DateTime.SpecifyKind(value - TimeZone.Value, DateTimeKind.Utc)); + + public SerializableDateTime ToLocal() + => new SerializableDateTime( + DateTime.SpecifyKind( + value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, + DateTimeKind.Local)); + + public long Ticks => value.Ticks; + + public DateTime ToUtcValue() => ToUtc().value; + public DateTime ToLocalValue() => ToLocal().value; + + public static SerializableDateTime FromLocalUnixTime(long unixTime) + => new SerializableDateTime(UnixEpoch(DateTimeKind.Local) + TimeSpan.FromSeconds(unixTime)); + + public static SerializableDateTime FromUtcUnixTime(long unixTime) + => new SerializableDateTime(UnixEpoch(DateTimeKind.Utc) + TimeSpan.FromSeconds(unixTime)); + + public long ToUnixTime() + => (value - UnixEpoch(value.Kind)).Ticks / TimeSpan.TicksPerSecond; + + private static string MakeString(params (long Value, string Suffix)[] parts) + => string.Join(' ', + parts.Select(p => $"{p.Value.ToString().PadLeft(2, '0')}{p.Suffix}")); + + public override string ToString() + => MakeString( + // Let's go out of our way to tag + // the year, month and day so nobody + // gets confused about the meaning of + // each number + (value.Year, "Y"), + (value.Month, "M"), + (value.Day, "D"), + + (value.Hour, "HR"), + (value.Minute, "MIN"), + (value.Second, "SEC")) + + $" {TimeZone}"; + + public string ToLocalUserString() + => ToLocalValue().ToString(CultureInfo.InvariantCulture); + + public override int GetHashCode() + => HashCode.Combine( + value.Year, value.Month, value.Day, + value.Hour, value.Minute, value.Second, + TimeZone.GetHashCode()); + + public static Option Parse(string str) + { + if (long.TryParse(str, out long unixTime) + && unixTime > 0 + && unixTime < (DateTime.MaxValue - UnixEpoch(DateTimeKind.Utc)).TotalSeconds) + { + return Option.Some(FromUtcUnixTime(unixTime)); + } + + string[] split = str.Split(' '); + + int year = 0; int month = 0; int day = 0; + int hour = 0; int minute = 0; int second = 0; + SerializableTimeZone timeZone = default; + foreach (var part in split) + { + if (SerializableTimeZone.Parse(part).TryUnwrap(out var parsedTimeZone)) + { + timeZone = parsedTimeZone; + continue; + } + + Identifier suffix = string.Join("", part.Where(char.IsLetter)).ToIdentifier(); + if (!part.EndsWith(suffix.Value)) { continue; } + if (!int.TryParse( + part[..^suffix.Value.Length], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int value)) + { + continue; + } + + if (suffix == "Y") { year = value; } + else if (suffix == "M") { month = value; } + else if (suffix == "D") { day = value; } + else if (suffix == "HR") { hour = value; } + else if (suffix == "MIN") { minute = value; } + else if (suffix == "SEC") { second = value; } + } + + if (year > 0 && month > 0 && day > 0) + { + return Option.Some( + new SerializableDateTime( + new DateTime(year, month, day, hour, minute, second), + timeZone)); + } + + return Option.None(); + } + + public int CompareTo(SerializableDateTime other) + => ToUtc().value.CompareTo(other.ToUtc().value); + + public static bool operator <(in SerializableDateTime a, in SerializableDateTime b) + => a.CompareTo(b) < 0; + + public static bool operator >(in SerializableDateTime a, in SerializableDateTime b) + => a.CompareTo(b) > 0; + + public static bool operator ==(in SerializableDateTime a, in SerializableDateTime b) + => a.CompareTo(b) == 0; + + public static bool operator !=(in SerializableDateTime a, in SerializableDateTime b) + => !(a == b); + + public static SerializableDateTime operator +(in SerializableDateTime dt, in TimeSpan ts) + => new SerializableDateTime(dt.value + ts, dt.TimeZone); + + public static SerializableDateTime operator -(in SerializableDateTime dt, in TimeSpan ts) + => new SerializableDateTime(dt.value - ts, dt.TimeZone); + + public static TimeSpan operator -(in SerializableDateTime a, in SerializableDateTime b) + => a.ToUtc().value - b.ToUtc().value; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index a7d065ed0..08254fa0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -29,49 +29,6 @@ namespace Barotrauma static partial class ToolBox { - internal static class Epoch - { - private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - /// - /// Returns the current Unix Epoch (Coordinated Universal Time) - /// - public static int NowUTC - { - get - { - return (int)(DateTime.UtcNow.Subtract(epoch).TotalSeconds); - } - } - - /// - /// Returns the current Unix Epoch (user's current time) - /// - public static int NowLocal - { - get - { - return (int)(DateTime.Now.Subtract(epoch).TotalSeconds); - } - } - - /// - /// Convert an epoch to a datetime - /// - public static DateTime ToDateTime(decimal unixTime) - { - return epoch.AddSeconds((long)unixTime); - } - - /// - /// Convert a DateTime to a unix time - /// - public static uint FromDateTime(DateTime dt) - { - return (uint)(dt.Subtract(epoch).TotalSeconds); - } - } - public static bool IsProperFilenameCase(string filename) { //File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds. diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index cd2fbd47e..8f63361d2 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,19 +1,60 @@ --------------------------------------------------------------------------------------------------------- -v0.21.1.0 (unstable) +v0.21.6.0 --------------------------------------------------------------------------------------------------------- +- Minor localization fixes. +- Fixed some occasional crashes in the character editor. + +--------------------------------------------------------------------------------------------------------- +v0.21.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed delete save button not working in the singleplayer "load game" menu. +- Fixed depleted fuel revolver round recipe requiring too many materials. +- Reduced medical item cooldown to 0.5 seconds. +- Minor fixes to Chinese localization. +- Fix bugged sufforing poisoning when it reaches the full strength. +- Fixed crashing when you save a sub that contains item variants with decorative sprites (most wrecks) as a new content package. + +--------------------------------------------------------------------------------------------------------- +v0.21.4.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when an item spawns after upgrades with material requirements have been purchased in the multiplayer campaign. +- Fixed crashing if the submarine preview is closed before generating the preview finishes. + Changes: -- Added translations for the submarine and character editors. - Some submarine upgrades cost materials in addition to money. +- Crush depths are displayed on the submarine switch terminal. - Miscellaneous fixes to all translations. - The "max missions" campaign setting is restricted to a maximum of 3. - Minor improvements to ragdoll animation when falling prone due to stun/unconsciousness/ragdolling. - Revisited the skill requirements and the OnFailure conditions on welding tool, plasma cutter, flamer, and steam gun. Flamer and steam gun now apply burns on the user only when they don't have enough skills to use the item. All these items are now less efficient when used with low skills. - Changed Alarm Buzzer sound to differentiate it from the diving suit warning beep. - Depth charge tweaks: require UEX, increase non-contained item explosion radius and damage, allow quality to affect explosion, increase pricing. -- Reduceed Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular. +- Reduced Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular. - Lights flicker when hit by EMP. - Large monsters no longer drop the loot when they die. This was implemented as a workaround to it being difficult to grab large monsters, but now they're much easier to grab - you can grab them anywhere near their main limb, instead of having to find the "origin" somewhere at the center of the monster. +- Bots now prefer PUCS over the other diving suits and the other diving suits over exosuit. +- Made drinks and other consumables work more consistently. +- Adjusted the aim poses used while holding certain melee weapons. +- Harpoons: Adjusted the damage on walls and the projectile launch forces. Spears can now stick to the walls. Inversed the reeling logic: ragdoll key = reel in. + +Poison overhaul: +- Reworked most poison afflictions with the goal that a medic can now use them offensively on monsters and humans alike. +- Sufforin, Morbusine, Cyanide, Paralyzant now work on most monsters. Bigger monsters are generally more resistant than the smaller. +- Sufforin poisoning: Slowly makes the target fall sick. Eventually leads to death. Stronger monsters may recover. +- Morbusine poisoning: Kills the target relatively quickly from gradual oxygen deprivation/organ damage. Stronger monsters may recover. +- Cyanide poisoning: Doesn't do much at first, but progresses rapidly and kills the target suddenly. Lethal even to the strongest monsters. +- Paralysis: Advances more quickly than previously. Effective even on larger monsters. (Leucocytes don't always apply enough paralysis for it to progress. They also use a version of the affliction that progresses using the old, slower speed.) +- Poisons (and paralysis) now wear off when the affliction level is low. +- Made medic AI react sooner to treat poisons. +- Attempts to poison the target can now fail when the user has a low medical skill. +- Added a short cooldown to applying medical items. Prevents being able to spam medicine (or poisons) at a nearly unlimited rate. +- Adjusted the syringe and stun gun dart trajectories. +- Reworked the weapons and the medical skill gains: instead of always increasing the weapons skill when a target takes damage, afflictions like poisons now increase the medical skill. Also applying the buffs now increases the medical skill. The skill increases are defined per affliction. +- Fixed the skill gains on low levels (0-15) being ridiculously high. Talents: - Fixed "Down With the Ship" sometimes having an incorrect description. @@ -31,11 +72,17 @@ Talents: - Fixed "Steady Tune" not doing what the description says (giving a constantly diminishing 7.5% resistance instead of 60%), made the talent give 100% immunity instead. - Fixed "Multifunctional" talent not giving a boost to crowbar damage. - Fixed inaccurate "Unstoppable Curiosity" and "Ph.D in Nuclear Physics" descriptions. +- Fixed skill boost from "Journeyman" not matching the value in the description. +- Fixed "Quickdraw" damage bonus. +- Fixed inaccurate "Journeyman" description. +- Fixed "Oiled Machinery" not increasing fabrication speed. +- Fixed tinkered doors requiring a higher condition percentage to become repaired. +- Fixed "Drunken Sailor" not protecting entirely from the negative effects of drunkness. Multiplayer: - Better support for playing the MP campaign without a host or someone with campaign management permissions on the server: - - You can vote to end the round in the campaign too. - - Automatic restart works in the campaign mode too. + - You can vote to end the round in the campaign as well. + - Automatic restart works in the campaign mode as well. - Anyone can manage salaries if there's no-one allowed to do it. - If there's no host or anyone with permissions in the server, anyone is able to setup a new campaign. - Campaign can be voted for when game mode voting is enabled. @@ -46,9 +93,16 @@ Multiplayer: - Fixed clients not entering the server lobby if they accept a server invite during a single player round or tutorial. - Fixed inability to join IPv4 servers when IPv6 is disabled. - Fixed hidden submarine list sometimes desyncing if you have specific custom submarines. +- Fixed characters spawning with their inventory, skills, etc intact if they die during a round and the round ends when the client is no longer in the server. Or, to put it another way, spawning as if they hadn't died at all. +- Fixed an issue that prevented some dedicated servers from appearing in the server list. +- Fixed chat-linked wifi components not receiving order and report messages. +- Save the server settings after starting up the server to create the default settings file if it doesn't exist, instead of only when the server is shut down or the settings changed. +- Fixed list of hidden submarines sometimes desyncing. +- Fixed inability to start Linux dedicated server using LinuxGSM due to an incorrect EOL character in DedicatedServer.exe. Bugfixes: - Fixed high-quality items spawning earlier in the campaign when playing with a higher campaign difficulty setting. +- Fixed multiple issues in the tutorials: missing text, events not progressing when not following tutorial's steps, infographics usability issues. - Fixed attacking with a melee weapon making you unable to turn (flip) for a while. - Fixed ragdolling affecting the character's velocity, allowing it to be used as a way to avoid fall damage. - Hull fixes to vanilla subs and wrecks. @@ -61,15 +115,40 @@ Bugfixes: - Fixed flares still emitting light after running out. - Fixed Electrical Discharge Coil preview not working in the sub editor. - Fixed alien flares not activating when clicking LMB. -- Fixed crawler's arms getting broken when the character flips in water. +- Fixed crawler's arms getting broken when the character flips (turns) in water. - Fixed the recycle recipe of flak explosive ammo. - Fixed misaligned shells in wrecked railgun shell rack. - Fixed misaligned light component light sprite. - Fixed crashing if the select audio device is disconnected while in the initial loading screen. - Fixed inability to sell genetic materials that aren't 100% refined. - Fixed liquid oxygenite exploding too easily. +- Fixed Hemulen's bottom airlock never fully draining. +- Fixed incorrect damage values in "Genetic Material (Hunter)" tooltip. +- Fixed exosuit not playing the warning beep when low or out of oxygen. +- Fixed "taste test" event showing for everyone and not progressing past the 1st prompt. +- Fixed "special training required" and "too many of this item" messages being shown to everyone when someone tries to place a portable pump. +- Added a pump to RemoraDrone's engine room, otherwise it's impossible to drain. +- Fixed some lighting sprites appearing dimmer than they should (most notably, junction boxes and supercapacitors). +- Fixed submarine's crush depth being displayed incorrectly on the campaign map when a submarine switch is pending: the hull upgrades of the current sub weren't taken into account, even though they carry over to the new sub. +- Fixed item assembly descriptions not showing up in the sub editor unless they're configured in a text file. +- Fixed rapid fissile accelerator ammo causing an explosion when launched, instead of when it hits something. +- Fixed bots sometimes trying to contain multiple ammo boxes when reloading turrets (also affected other items). +- Fixed "Manually Outfitted" not doing anything when starting a campaign. +- Fixed guardians trying to heal themselves in inactive pods (not destroyed pods, but ones deactivated via wiring). +- Fixed bots having trouble with fixing Barsuk's top hatch and leaks around it. +- Fixed sprite bleed in turret icons. +- Fixed radio channel presets resetting between rounds. +- Fixed some lines in the "shockjock" event being shown to everyone. +- Fixed radio noise playing even if you don't have a headset. +- Fixed hitscan projectiles going through subs when you're firing from inside another sub. +- Fixed lights set to flicker/pulse eventually getting out of sync even if they're set to the same frequency. +- Fixes to submarine preview: wires are now visible, scaled doors work correctly, camera is now correctly centered on the sub when opening the preview. +- Fixed the affliction type of some afflictions (like drunk) being "poison", causing them to be treated as poisons. They are now "debuffs" instead. +- Fixed defense bots not attacking pirates or bandits. Also fixed them not protecting the owner when attacked by the outpost guards. +- Fixed bots not knowing how to handle diving suits that don't have an oxygen tank inside them. Now they should be able to use them and refill them with an oxygen tank. Modding: +- Added a button for updating core mods into the Mods menu. - Addressed various inconsistencies, issues and limitations in how status effects are used in certain cases: - Status effects defined for attacks with the type "UseTarget" now correctly target the use target, instead of the attacker like they used to. - Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon and Projectile components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons. @@ -80,6 +159,10 @@ Modding: - Fixed crashing when trying to place a wreck with no hulls in a level. - Fixed mod descriptions getting truncated to 255 characters when selecting an already-published item in the Mods menu. - Fixed HMG's requiring hmgmagazine instead of any item with the type "hmgammo", making the use of modded ammo impossible without overriding the gun too. +- Fixed crashing when an upgrade tries to increase an integer value. +- Changed the affliction type of some afflictions, which might have implications on mods if they targeted them by type. +- Added AimAngle on Holdable item components. Can be used for defining a different hold angle for the aim and the rest poses. Note that on Holdables, the angles are mutually exclusive (defined separately), but on MeleeWeapons they are cumulative (added together). Therefore, no need to change the existing items! +- Fixed definitions not triggering the status effects that have the target type "UseTarget". --------------------------------------------------------------------------------------------------------- v0.20.16.1 diff --git a/Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs b/Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs new file mode 100644 index 000000000..e54c33018 --- /dev/null +++ b/Barotrauma/BarotraumaTest/CoordinateSpace2DTests.cs @@ -0,0 +1,55 @@ +using Barotrauma.Utils; +using FluentAssertions; +using FsCheck; +using Microsoft.Xna.Framework; +using System; +using Xunit; +namespace TestProject; + +public class CoordinateSpace2DTests +{ + class CustomGenerators + { + public static Arbitrary Vector2Generator() + { + const int intRange = 1 << 22; + const float intToFloat = 1 << 19; + + return Arb.From( + from int x in Gen.Choose(-intRange, intRange) + from int y in Gen.Choose(-intRange, intRange) + select new Vector2(x / intToFloat, y / intToFloat)); + } + } + + public CoordinateSpace2DTests() + { + Arb.Register(); + } + + [Fact] + public void TestLocalToCanonical() + { + void testCase(Tuple args) + { + var (vector, origin, i, j) = args; + + if (Vector2.DistanceSquared(i, j) <= 0.01f) { return; } + + var space = new CoordinateSpace2D + { + Origin = origin, + I = i, + J = j + }; + + Assert.True(Vector2.DistanceSquared( + Vector2.Transform(vector, space.LocalToCanonical), + origin + vector.X * i + vector.Y * j) < 0.01f); + } + + Prop.ForAll( + Arb.Generate>().ToArbitrary(), + testCase).QuickCheckThrowOnFailure(); + } +} diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs index 86213b54d..76d2a689c 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs @@ -10,7 +10,7 @@ using Xunit; namespace TestProject; -public class INetSerializableStructImplementationChecks +public sealed class INetSerializableStructImplementationChecks { private delegate bool TryFindBehaviorDelegate(Type type, out NetSerializableProperties.IReadWriteBehavior behavior); @@ -49,7 +49,7 @@ public class INetSerializableStructImplementationChecks viableArguments.AddRange(new[] { typeof(Vector2), - typeof(Point), + typeof(float), typeof(int) }); } diff --git a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs new file mode 100644 index 000000000..12cdb9243 --- /dev/null +++ b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics; +using Barotrauma; +using FluentAssertions; +using FsCheck; +using Xunit; + +namespace TestProject; + +public sealed class SerializableDateTimeTests +{ + private class CustomGenerators + { + private const short MinutesPerDay = 24 * 60; + private const int SecondsPerDay = MinutesPerDay * 60; + + public static Arbitrary SerializableDateTimeGenerator() + { + return Arb.From( + from int dateTimeDay in Gen.Choose(0, (int)(DateTime.MaxValue.Ticks / TimeSpan.TicksPerDay)) + from int dateTimeSeconds in Gen.Choose(0, SecondsPerDay) + from int timeZoneMinutes in Gen.Choose(-MinutesPerDay / 2, MinutesPerDay / 2) + select new SerializableDateTime( + DateTime.MinValue + TimeSpan.FromDays(dateTimeDay) + TimeSpan.FromSeconds(dateTimeSeconds), + new SerializableTimeZone(TimeSpan.FromMinutes(timeZoneMinutes)))); + } + } + + public SerializableDateTimeTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void EqualityTest() + { + Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); + } + + [Fact] + public void ParseTest() + { + var parseTest = "9369Y 09M 06D 03HR 43MIN 09SEC UTC+8:49"; + SerializableDateTime.Parse(parseTest); + Prop.ForAll(ParseCheck).QuickCheckThrowOnFailure(); + } + + private static void EqualityCheck(SerializableDateTime original) + { + var local = original.ToLocal(); + var utc = original.ToUtc(); + original.Should().BeEquivalentTo(local); + original.Should().BeEquivalentTo(utc); + local.Should().BeEquivalentTo(utc); + } + + private static void ParseCheck(SerializableDateTime original) + { + var str = original.ToString(); + SerializableDateTime.Parse(str).TryUnwrap(out var parsedTime).Should().BeTrue(); + parsedTime.Should().BeEquivalentTo(original); + } +} diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs index 6b36c44e1..479273e9e 100644 --- a/Barotrauma/BarotraumaTest/TestProject.cs +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -10,8 +10,8 @@ namespace TestProject { public static Arbitrary Vector2Generator() { - return Arb.From(from int x in Arb.Generate() - from int y in Arb.Generate() + return Arb.From(from float x in Arb.Generate().Where(f => !float.IsNaN(f) && !float.IsInfinity(f)) + from float y in Arb.Generate().Where(f => !float.IsNaN(f) && !float.IsInfinity(f)) select new Vector2(x, y)); } diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs index 9ca64ea88..28c80c246 100644 --- a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs @@ -7,22 +7,25 @@ namespace Steamworks public byte[] Data; public uint Handle; - /// - /// Cancels a ticket. - /// You should cancel your ticket when you close the game or leave a server. - /// - public void Cancel() - { - if ( Handle != 0 ) - { - SteamUser.Internal.CancelAuthTicket( Handle ); - } + public bool Canceled { get; private set; } - Handle = 0; - Data = null; - } + /// + /// Cancels a ticket. + /// You should cancel your ticket when you close the game or leave a server. + /// + public void Cancel() + { + if (Handle != 0) + { + SteamUser.Internal.CancelAuthTicket(Handle); + } - public void Dispose() + Handle = 0; + Data = null; + Canceled = true; + } + + public void Dispose() { Cancel(); } diff --git a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs index ea0bbd412..9f0feeaf2 100644 --- a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs +++ b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs @@ -2,7 +2,7 @@ The MIT License (MIT) -Copyright (c) 2013-2019 Riley Labrecque +Copyright (c) 2013-2022 Riley Labrecque Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,48 +25,60 @@ THE SOFTWARE. **/ using System; -using System.Net; using System.Runtime.InteropServices; namespace Steamworks { //----------------------------------------------------------------------------- - // Purpose: Callback interface for receiving responses after pinging an individual server + // Purpose: Callback interface for receiving responses after requesting rules + // details on a particular server. // // These callbacks all occur in response to querying an individual server - // via the ISteamMatchmakingServers()->PingServer() call below. If you are - // destructing an object that implements this interface then you should call + // via the ISteamMatchmakingServers()->ServerRules() call below. If you are + // destructing an object that implements this interface then you should call // ISteamMatchmakingServers()->CancelServerQuery() passing in the handle to the query // which is in progress. Failure to cancel in progress queries when destructing // a callback handler may result in a crash when a callback later occurs. //----------------------------------------------------------------------------- - public class SteamMatchmakingPingResponse + public class SteamMatchmakingRulesResponse { - // Server has responded successfully and has updated data - public delegate void ServerResponded(Steamworks.Data.ServerInfo server); + // Got data on a rule on the server -- you'll get one of these per rule defined on + // the server you are querying + public delegate void RulesResponded(string pchRule, string pchValue); - // Server failed to respond to the ping request - public delegate void ServerFailedToRespond(); + // The server failed to respond to the request for rule details + public delegate void RulesFailedToRespond(); - private VTable m_VTable; - private IntPtr m_pVTable; + // The server has finished responding to the rule details request + // (ie, you won't get anymore RulesResponded callbacks) + public delegate void RulesRefreshComplete(); + + private readonly VTable m_VTable; + private readonly IntPtr m_pVTable; private GCHandle m_pGCHandle; - private ServerResponded m_ServerResponded; - private ServerFailedToRespond m_ServerFailedToRespond; + private readonly RulesResponded m_RulesResponded; + private readonly RulesFailedToRespond m_RulesFailedToRespond; + private readonly RulesRefreshComplete m_RulesRefreshComplete; - public SteamMatchmakingPingResponse(ServerResponded onServerResponded, ServerFailedToRespond onServerFailedToRespond) + public SteamMatchmakingRulesResponse( + RulesResponded onRulesResponded, + RulesFailedToRespond onRulesFailedToRespond, + RulesRefreshComplete onRulesRefreshComplete) { - if (onServerResponded == null || onServerFailedToRespond == null) + if (onRulesResponded == null || onRulesFailedToRespond == null || onRulesRefreshComplete == null) { throw new ArgumentNullException(); } - m_ServerResponded = onServerResponded; - m_ServerFailedToRespond = onServerFailedToRespond; + + m_RulesResponded = onRulesResponded; + m_RulesFailedToRespond = onRulesFailedToRespond; + m_RulesRefreshComplete = onRulesRefreshComplete; m_VTable = new VTable() { - m_VTServerResponded = InternalOnServerResponded, - m_VTServerFailedToRespond = InternalOnServerFailedToRespond, + m_VTRulesResponded = InternalOnRulesResponded, + m_VTRulesFailedToRespond = InternalOnRulesFailedToRespond, + m_VTRulesRefreshComplete = InternalOnRulesRefreshComplete }; m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); Marshal.StructureToPtr(m_VTable, m_pVTable, false); @@ -74,21 +86,7 @@ namespace Steamworks m_pGCHandle = GCHandle.Alloc(m_pVTable, GCHandleType.Pinned); } - private Data.HServerQuery hserverPing = 0; - public bool QueryActive { get { return hserverPing != 0; } } - - public void Cancel() - { - if (hserverPing != 0) { ServerList.Base.Internal.CancelServerQuery(hserverPing); } - hserverPing = 0; - } - - public void HQueryPing(IPAddress ip, int port) - { - hserverPing = ServerList.Base.Internal.PingServer(ip.IpToInt32(), (ushort)port, (IntPtr)this); - } - - ~SteamMatchmakingPingResponse() + ~SteamMatchmakingRulesResponse() { if (m_pVTable != IntPtr.Zero) { @@ -102,48 +100,69 @@ namespace Steamworks } #if NOTHISPTR - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate void InternalServerResponded(gameserveritem_t server); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate void InternalServerFailedToRespond(); - private void InternalOnServerResponded(gameserveritem_t server) { - m_ServerResponded(server); - } - private void InternalOnServerFailedToRespond() { - m_ServerFailedToRespond(); - } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void InternalRulesResponded(IntPtr pchRule, IntPtr pchValue); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void InternalRulesFailedToRespond(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void InternalRulesRefreshComplete(); + + private void InternalOnRulesResponded(IntPtr pchRule, IntPtr pchValue) + { + m_RulesResponded(Helpers.MemoryToString(pchRule), Helpers.MemoryToString(pchValue)); + } + + private void InternalOnRulesFailedToRespond() + { + m_RulesFailedToRespond(); + } + + private void InternalOnRulesRefreshComplete() + { + m_RulesRefreshComplete(); + } #else [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate void InternalServerResponded(IntPtr thisptr, Steamworks.Data.gameserveritem_t server); + public delegate void InternalRulesResponded(IntPtr thisptr, IntPtr pchRule, IntPtr pchValue); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate void InternalServerFailedToRespond(IntPtr thisptr); - private void InternalOnServerResponded(IntPtr thisptr, Steamworks.Data.gameserveritem_t server) - { - hserverPing = 0; + public delegate void InternalRulesFailedToRespond(IntPtr thisptr); - m_ServerResponded(Steamworks.Data.ServerInfo.From(server)); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalRulesRefreshComplete(IntPtr thisptr); + + private void InternalOnRulesResponded(IntPtr thisptr, IntPtr pchRule, IntPtr pchValue) + { + m_RulesResponded(Helpers.MemoryToString(pchRule), Helpers.MemoryToString(pchValue)); } - private void InternalOnServerFailedToRespond(IntPtr thisptr) - { - hserverPing = 0; - m_ServerFailedToRespond(); + private void InternalOnRulesFailedToRespond(IntPtr thisptr) + { + m_RulesFailedToRespond(); + } + + private void InternalOnRulesRefreshComplete(IntPtr thisptr) + { + m_RulesRefreshComplete(); } #endif [StructLayout(LayoutKind.Sequential)] private class VTable { - [NonSerialized] - [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalServerResponded m_VTServerResponded; + [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesResponded m_VTRulesResponded; - [NonSerialized] - [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalServerFailedToRespond m_VTServerFailedToRespond; + [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesFailedToRespond m_VTRulesFailedToRespond; + + [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesRefreshComplete m_VTRulesRefreshComplete; } - public static explicit operator System.IntPtr(SteamMatchmakingPingResponse that) + public static explicit operator System.IntPtr(SteamMatchmakingRulesResponse that) { return that.m_pGCHandle.AddrOfPinnedObject(); } diff --git a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs index e2f02d7f0..bc1c4d848 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -1,209 +1,76 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Threading.Tasks; -using Steamworks.Data; namespace Steamworks { internal static class SourceServerQuery { - private static readonly byte[] A2S_SERVERQUERY_GETCHALLENGE = { 0x55, 0xFF, 0xFF, 0xFF, 0xFF }; - // private static readonly byte A2S_PLAYER = 0x55; - private const byte A2S_RULES = 0x56; - - private static readonly Dictionary>> PendingQueries = - new Dictionary>>(); - - private static HashSet activeRequests = new HashSet(); - private static int lastRequestId = 0; - - internal static Task> GetRules( ServerInfo server ) - { - var endpoint = new IPEndPoint(server.Address, server.QueryPort); - - lock (PendingQueries) - { - if (PendingQueries.TryGetValue(endpoint, out var pending)) - return pending; - - var task = GetRulesImpl( endpoint ) - .ContinueWith(t => - { - lock (PendingQueries) - { - PendingQueries.Remove(endpoint); - } - - return t; - }) - .Unwrap(); - - PendingQueries.Add(endpoint, task); - return task; - } - } - - private static async Task> GetRulesImpl( IPEndPoint endpoint ) - { - int currId; - lock (activeRequests) - { - lastRequestId++; - currId = lastRequestId; - activeRequests.Add(currId); - } - - try - { - await Task.Yield(); - while (true) - { - lock (activeRequests) - { - if (!activeRequests.Any() || (currId - activeRequests.Min()) < 25) { break; } - } - await Task.Delay(25); - } - - using (var client = new UdpClient()) - { - client.Client.SendTimeout = 3000; - client.Client.ReceiveTimeout = 3000; - client.Connect(endpoint); - - return await GetRules(client); - } - } - catch (System.Exception) - { - //Console.Error.WriteLine( e.Message ); - return null; - } - finally - { - lock (activeRequests) - { - activeRequests.Remove(currId); - } - } + private enum Status + { + Pending, + Failure, + Success } - static async Task> GetRules( UdpClient client ) + private static readonly HashSet ruleResponseHandlers + = new HashSet(); + + internal static async Task> GetRules(Steamworks.Data.ServerInfo server) { - var challengeBytes = await GetChallengeData( client ); - challengeBytes[0] = A2S_RULES; - await Send( client, challengeBytes ); - var ruleData = await Receive( client ); + Status status = Status.Pending; var rules = new Dictionary(); - using ( var br = new BinaryReader( new MemoryStream( ruleData ) ) ) - { - if ( br.ReadByte() != 0x45 ) - throw new Exception( "Invalid data received in response to A2S_RULES request" ); + SteamMatchmakingRulesResponse responseHandler = null; - var numRules = br.ReadUInt16(); - for ( int index = 0; index < numRules; index++ ) - { - rules.Add( br.ReadNullTerminatedUTF8String(), br.ReadNullTerminatedUTF8String() ); - } + void onRulesResponded(string key, string value) + => rules.Add(key, value); + + void onRulesFailToRespond() + { + finish(Status.Failure); } - return rules; - } - - - - static async Task Receive( UdpClient client ) - { - byte[][] packets = null; - byte packetNumber = 0, packetCount = 1; - - do + void onRulesRefreshComplete() { - Task result = client.ReceiveAsync(); - await Task.WhenAny(result, Task.Delay(3000)); - if (!result.IsCompleted) - { - throw new Exception("Receive timed out"); - } - var buffer = result.Result.Buffer; - - using ( var br = new BinaryReader( new MemoryStream( buffer ) ) ) - { - var header = br.ReadInt32(); - - if ( header == -1 ) - { - var unsplitdata = new byte[buffer.Length - br.BaseStream.Position]; - Buffer.BlockCopy( buffer, (int)br.BaseStream.Position, unsplitdata, 0, unsplitdata.Length ); - return unsplitdata; - } - else if ( header == -2 ) - { - int requestId = br.ReadInt32(); - packetNumber = br.ReadByte(); - packetCount = br.ReadByte(); - int splitSize = br.ReadInt32(); - } - else - { - throw new System.Exception( "Invalid Header" ); - } - - if ( packets == null ) packets = new byte[packetCount][]; - - var data = new byte[buffer.Length - br.BaseStream.Position]; - Buffer.BlockCopy( buffer, (int)br.BaseStream.Position, data, 0, data.Length ); - packets[packetNumber] = data; - } + finish(Status.Success); } - while ( packets.Any( p => p == null ) ); - var combinedData = Combine( packets ); - return combinedData; - } - - private static async Task GetChallengeData( UdpClient client ) - { - await Send( client, A2S_SERVERQUERY_GETCHALLENGE ); - - var challengeData = await Receive( client ); - - if ( challengeData[0] != 0x41 ) - throw new Exception( "Invalid Challenge" ); - - return challengeData; - } - - static async Task Send( UdpClient client, byte[] message ) - { - var sendBuffer = new byte[message.Length + 4]; - - sendBuffer[0] = 0xFF; - sendBuffer[1] = 0xFF; - sendBuffer[2] = 0xFF; - sendBuffer[3] = 0xFF; - - Buffer.BlockCopy( message, 0, sendBuffer, 4, message.Length ); - - await client.SendAsync( sendBuffer, message.Length + 4 ); - } - - static byte[] Combine( byte[][] arrays ) - { - var rv = new byte[arrays.Sum( a => a.Length )]; - int offset = 0; - foreach ( byte[] array in arrays ) + void finish(Status stat) { - Buffer.BlockCopy( array, 0, rv, offset, array.Length ); - offset += array.Length; + if (status == Status.Pending) { status = stat; } + + var handler = responseHandler; + if (handler is null) { return; } + + lock (ruleResponseHandlers) + { + ruleResponseHandlers.Remove(handler); + } + responseHandler = null; } - return rv; + + responseHandler = new SteamMatchmakingRulesResponse( + onRulesResponded, + onRulesFailToRespond, + onRulesRefreshComplete); + lock (ruleResponseHandlers) + { + ruleResponseHandlers.Add(responseHandler); + } + + var query = SteamMatchmakingServers.Internal.ServerRules( + server.AddressRaw, (ushort)server.QueryPort, (IntPtr)responseHandler); + + while (status == Status.Pending) + { + await Task.Delay(25); + } + + SteamMatchmakingServers.Internal.CancelServerQuery(query); + + return status == Status.Success ? rules : null; } }; From 9470edead3ffebbcc3bef2e3a189793f9577c62b Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 31 Mar 2023 18:40:44 +0300 Subject: [PATCH 03/14] Build 1.1.4.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 12 +- .../ClientSource/CameraTransition.cs | 40 +- .../Characters/AI/Wreck/WreckAI.cs | 16 +- .../Characters/Animation/Ragdoll.cs | 14 +- .../ClientSource/Characters/Character.cs | 44 +- .../ClientSource/Characters/CharacterHUD.cs | 211 +++- .../ClientSource/Characters/CharacterInfo.cs | 30 +- .../Characters/CharacterNetworking.cs | 1 + .../Characters/Health/CharacterHealth.cs | 43 +- .../ClientSource/Characters/Limb.cs | 22 +- .../ClientSource/DebugConsole.cs | 92 +- .../Events/EventActions/ConversationAction.cs | 55 +- .../ClientSource/Events/EventManager.cs | 104 +- .../Missions/AbandonedOutpostMission.cs | 7 +- .../Events/Missions/CargoMission.cs | 3 +- .../Events/Missions/EndMission.cs | 138 +++ .../Events/Missions/GoToMission.cs | 2 +- .../Events/Missions/MineralMission.cs | 4 +- .../ClientSource/Events/Missions/Mission.cs | 76 +- .../Events/Missions/MissionMode.cs | 1 + .../Events/Missions/MissionPrefab.cs | 58 +- .../Events/Missions/MonsterMission.cs | 11 +- .../Events/Missions/SalvageMission.cs | 69 +- .../ClientSource/GUI/ComponentStyle.cs | 8 +- .../ClientSource/GUI/CrewManagement.cs | 120 ++- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 165 ++-- .../ClientSource/GUI/GUICanvas.cs | 42 +- .../ClientSource/GUI/GUIDropDown.cs | 14 +- .../ClientSource/GUI/GUIListBox.cs | 1 + .../ClientSource/GUI/GUIMessageBox.cs | 2 +- .../ClientSource/GUI/GUINumberInput.cs | 4 +- .../ClientSource/GUI/GUIPrefab.cs | 31 +- .../ClientSource/GUI/GUITextBox.cs | 2 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 39 +- .../ClientSource/GUI/LoadingScreen.cs | 91 +- .../ClientSource/GUI/MedicalClinicUI.cs | 125 ++- .../ClientSource/GUI/Store.cs | 92 +- .../ClientSource/GUI/SubmarineSelection.cs | 80 +- .../ClientSource/GUI/TabMenu.cs | 47 +- .../ClientSource/GUI/TalentMenu.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 35 +- .../ClientSource/GUI/VotingInterface.cs | 55 +- .../BarotraumaClient/ClientSource/GameMain.cs | 20 +- .../ClientSource/GameSession/CrewManager.cs | 15 +- .../GameSession/Data/CampaignMetadata.cs | 39 +- .../ClientSource/GameSession/Data/Wallet.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 119 ++- .../GameModes/MultiPlayerCampaign.cs | 188 ++-- .../GameModes/SinglePlayerCampaign.cs | 256 +++-- .../ClientSource/GameSession/HintManager.cs | 2 +- .../ClientSource/GameSession/MedicalClinic.cs | 15 + .../ClientSource/GameSession/ReadyCheck.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 197 ++-- .../ClientSource/Items/CharacterInventory.cs | 158 +-- .../ClientSource/Items/Components/Door.cs | 26 +- .../Items/Components/ElectricalDischarger.cs | 5 + .../Items/Components/Holdable/RangedWeapon.cs | 9 +- .../Items/Components/ItemComponent.cs | 23 +- .../Items/Components/ItemContainer.cs | 104 +- .../Items/Components/LightComponent.cs | 31 +- .../Items/Components/Machines/Fabricator.cs | 14 +- .../Items/Components/Machines/MiniMap.cs | 18 +- .../Items/Components/Machines/Sonar.cs | 106 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/Projectile.cs | 1 + .../Items/Components/Repairable.cs | 12 +- .../ClientSource/Items/Components/Rope.cs | 1 + .../Items/Components/Signal/Connection.cs | 2 +- .../Components/Signal/ConnectionPanel.cs | 6 +- .../Components/Signal/CustomInterface.cs | 81 +- .../Items/Components/Signal/Terminal.cs | 57 +- .../Items/Components/StatusHUD.cs | 15 +- .../ClientSource/Items/Components/Turret.cs | 23 +- .../ClientSource/Items/Inventory.cs | 59 +- .../ClientSource/Items/Item.cs | 68 +- .../ClientSource/Items/ItemPrefab.cs | 32 +- .../BarotraumaClient/ClientSource/Map/Gap.cs | 41 + .../BarotraumaClient/ClientSource/Map/Hull.cs | 17 +- .../ClientSource/Map/Levels/Level.cs | 6 +- .../Map/Levels/LevelObjects/LevelObject.cs | 9 +- .../Levels/LevelObjects/LevelObjectManager.cs | 107 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 95 +- .../ClientSource/Map/Lights/ConvexHull.cs | 617 ++++-------- .../ClientSource/Map/Lights/LightManager.cs | 144 ++- .../ClientSource/Map/Lights/LightSource.cs | 603 ++++++----- .../ClientSource/Map/LinkedSubmarine.cs | 4 +- .../ClientSource/Map/Map/Map.cs | 496 +++++++--- .../ClientSource/Map/MapEntity.cs | 36 +- .../ClientSource/Map/Structure.cs | 27 +- .../ClientSource/Map/Submarine.cs | 7 + .../ClientSource/Map/SubmarinePreview.cs | 2 +- .../ClientSource/Map/WayPoint.cs | 37 +- .../ClientSource/Networking/Client.cs | 2 +- .../Networking/FileTransfer/FileReceiver.cs | 2 - .../ClientSource/Networking/GameClient.cs | 96 +- .../Networking/Primitives/Peers/ClientPeer.cs | 4 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 2 +- .../Networking/ServerList/ServerInfo.cs | 50 +- .../SteamDedicatedServerProvider.cs | 2 +- .../ServerProviders/SteamP2PServerProvider.cs | 4 +- .../ClientSource/Networking/ServerSettings.cs | 78 +- .../Networking/Voip/VoipCapture.cs | 2 + .../Networking/Voip/VoipClient.cs | 6 +- .../ClientSource/Networking/Voting.cs | 79 +- .../ClientSource/Particles/ParticleEmitter.cs | 5 +- .../ClientSource/Particles/ParticleManager.cs | 26 +- .../ClientSource/Particles/ParticlePrefab.cs | 3 + .../ClientSource/Physics/PhysicsBody.cs | 26 +- .../ClientSource/PlayerInput.cs | 111 +-- .../BarotraumaClient/ClientSource/Program.cs | 5 +- .../ClientSource/Screens/CampaignEndScreen.cs | 82 +- .../CampaignSetupUI/CampaignSetupUI.cs | 83 +- .../SinglePlayerCampaignSetupUI.cs | 27 +- .../ClientSource/Screens/CampaignUI.cs | 147 ++- .../CharacterEditor/CharacterEditorScreen.cs | 131 +-- .../ClientSource/Screens/CreditsPlayer.cs | 10 +- .../ClientSource/Screens/GameScreen.cs | 55 +- .../ClientSource/Screens/LevelEditorScreen.cs | 10 +- .../ClientSource/Screens/MainMenuScreen.cs | 163 +-- .../ClientSource/Screens/ModDownloadScreen.cs | 5 +- .../ClientSource/Screens/NetLobbyScreen.cs | 18 +- .../Screens/ParticleEditorScreen.cs | 6 +- .../ServerListScreen/ServerListScreen.cs | 252 ++++- .../ClientSource/Screens/SlideshowPlayer.cs | 173 ++++ .../ClientSource/Screens/SubEditorScreen.cs | 77 +- .../ClientSource/Screens/TestScreen.cs | 2 +- .../Settings/ServerListFilters.cs | 24 +- .../ClientSource/Sounds/SoundPlayer.cs | 55 +- .../ClientSource/Sounds/SoundPrefab.cs | 17 +- .../ClientSource/Sprite/ConditionalSprite.cs | 6 +- .../ClientSource/Sprite/DecorativeSprite.cs | 9 +- .../StatusEffects/StatusEffect.cs | 36 +- .../ClientSource/Steam/BulkDownloader.cs | 2 +- .../ClientSource/Steam/Lobby.cs | 4 +- .../ClientSource/Steam/SteamManager.cs | 6 +- .../ClientSource/Steam/Workshop.cs | 2 +- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 20 +- .../WorkshopMenu/Mutable/ModListPreset.cs | 2 +- .../ClientSource/Utils/WikiImage.cs | 16 +- .../Content/Effects/damageshader.xnb | Bin 1355 -> 2340 bytes .../Content/Effects/damageshader_opengl.xnb | Bin 1359 -> 2274 bytes .../Content/Effects/losshader.xnb | Bin 1470 -> 1962 bytes .../Content/Effects/wearableclip.xnb | Bin 2056 -> 2416 bytes .../Content/Effects/wearableclip_opengl.xnb | Bin 1842 -> 2380 bytes .../BarotraumaClient/LinuxClient.csproj | 4 +- Barotrauma/BarotraumaClient/MacClient.csproj | 4 +- .../BarotraumaClient/Shaders/damageshader.fx | 23 +- .../Shaders/damageshader_opengl.fx | 23 +- .../BarotraumaClient/Shaders/losshader.fx | 13 +- .../BarotraumaClient/Shaders/wearableclip.fx | 8 + .../Shaders/wearableclip_opengl.fx | 8 + .../BarotraumaClient/WindowsClient.csproj | 4 +- .../BarotraumaServer/LinuxServer.csproj | 4 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/CharacterInfo.cs | 7 +- .../Characters/CharacterNetworking.cs | 1 + .../ServerSource/DebugConsole.cs | 83 +- .../Events/EventActions/ConversationAction.cs | 16 +- .../Events/EventActions/MissionAction.cs | 43 + .../ServerSource/Events/EventManager.cs | 12 +- .../Events/Missions/EndMission.cs | 19 + .../Events/Missions/MineralMission.cs | 12 +- .../Events/Missions/SalvageMission.cs | 67 +- .../BarotraumaServer/ServerSource/GameMain.cs | 15 +- .../GameModes/MultiPlayerCampaign.cs | 151 ++- .../ServerSource/GameSession/MedicalClinic.cs | 81 +- .../Items/Components/Projectile.cs | 5 +- .../Components/Signal/ConnectionPanel.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 2 +- .../ServerSource/Items/Inventory.cs | 26 + .../ServerSource/Items/Item.cs | 4 +- .../ServerSource/Networking/BanList.cs | 3 +- .../ServerSource/Networking/ChatMessage.cs | 6 +- .../ServerSource/Networking/GameServer.cs | 229 ++--- .../ServerSource/Networking/KarmaManager.cs | 10 +- .../Peers/Server/LidgrenServerPeer.cs | 6 +- .../Primitives/Peers/Server/ServerPeer.cs | 4 +- .../ServerSource/Networking/RespawnManager.cs | 5 +- .../ServerSource/Networking/ServerSettings.cs | 19 +- .../ServerSource/Networking/Voting.cs | 31 +- .../BarotraumaServer/ServerSource/Program.cs | 5 +- .../ServerSource/Screens/NetLobbyScreen.cs | 26 +- .../ServerSource/Steam/SteamManager.cs | 21 +- .../ServerSource/Utils/DoSProtection.cs | 232 +++++ .../ServerSource/Utils/RateLimiter.cs | 135 +++ .../BarotraumaServer/WindowsServer.csproj | 4 +- .../BarotraumaShared/Data/languageoptions.xml | 22 + .../Data/permissionpresets.xml | 2 +- .../Characters/AI/AIController.cs | 3 +- .../Characters/AI/EnemyAIController.cs | 379 ++++--- .../Characters/AI/HumanAIController.cs | 210 ++-- .../Characters/AI/IndoorsSteeringManager.cs | 24 +- .../SharedSource/Characters/AI/LatchOntoAI.cs | 2 +- .../Characters/AI/MentalStateManager.cs | 1 - .../Characters/AI/NPCConversation.cs | 11 +- .../Characters/AI/Objectives/AIObjective.cs | 4 +- .../AI/Objectives/AIObjectiveCombat.cs | 21 +- .../AI/Objectives/AIObjectiveContainItem.cs | 4 +- .../Objectives/AIObjectiveExtinguishFire.cs | 68 +- .../Objectives/AIObjectiveFightIntruders.cs | 10 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 43 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 24 +- .../AI/Objectives/AIObjectiveGetItem.cs | 25 +- .../AI/Objectives/AIObjectiveGoTo.cs | 81 +- .../AI/Objectives/AIObjectiveIdle.cs | 6 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 2 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 11 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 34 +- .../AI/Objectives/AIObjectiveRescue.cs | 4 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 4 +- .../AI/Objectives/AIObjectiveReturn.cs | 3 + .../SharedSource/Characters/AI/Order.cs | 4 + .../SharedSource/Characters/AI/PathFinder.cs | 1 + .../AI/ShipCommand/ShipIssueWorker.cs | 3 +- .../ShipIssueWorkerOperateWeapons.cs | 15 +- .../Characters/AI/ShipCommandManager.cs | 15 +- .../Characters/AI/SwarmBehavior.cs | 8 +- .../Characters/AI/Wreck/WreckAI.cs | 257 +++-- .../Characters/Animation/AnimController.cs | 4 +- .../Animation/FishAnimController.cs | 135 ++- .../Animation/HumanoidAnimController.cs | 92 +- .../Characters/Animation/Ragdoll.cs | 138 ++- .../SharedSource/Characters/Attack.cs | 25 +- .../SharedSource/Characters/Character.cs | 364 +++---- .../SharedSource/Characters/CharacterInfo.cs | 75 +- .../Characters/CharacterPrefab.cs | 10 +- .../Health/Afflictions/Affliction.cs | 103 +- .../Health/Afflictions/AfflictionBleeding.cs | 3 + .../Health/Afflictions/AfflictionHusk.cs | 11 +- .../Health/Afflictions/AfflictionPrefab.cs | 620 +++++++++--- .../Health/Afflictions/AfflictionPsychosis.cs | 3 + .../Afflictions/AfflictionSpaceHerpes.cs | 7 +- .../Health/Buffs/BuffDurationIncrease.cs | 18 +- .../Characters/Health/CharacterHealth.cs | 110 ++- .../SharedSource/Characters/HumanPrefab.cs | 62 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 37 +- .../SharedSource/Characters/Jobs/Skill.cs | 2 +- .../SharedSource/Characters/Limb.cs | 49 +- .../Params/Animation/FishAnimations.cs | 3 + .../Characters/Params/CharacterParams.cs | 44 +- .../Params/Ragdoll/RagdollParams.cs | 25 +- .../AbilityConditionCharacter.cs | 10 +- .../AbilityConditionMission.cs | 42 +- .../AbilityConditionHasPermanentStat.cs | 1 - ...erAbilityUnlockApprenticeshipTalentTree.cs | 35 +- .../Characters/Talents/TalentTree.cs | 14 + .../ContentFile/SlideshowsFile.cs | 15 + .../ContentPackageManager.cs | 7 +- .../ContentManagement/ContentXElement.cs | 14 +- .../SharedSource/DebugConsole.cs | 44 +- .../BarotraumaShared/SharedSource/Enums.cs | 495 +++++++++- .../SharedSource/Events/ArtifactEvent.cs | 2 +- .../SharedSource/Events/Event.cs | 2 + .../EventActions/CheckConditionalAction.cs | 12 +- .../Events/EventActions/CheckDataAction.cs | 42 +- .../Events/EventActions/CheckItemAction.cs | 10 +- .../Events/EventActions/ConversationAction.cs | 2 +- .../Events/EventActions/MissionAction.cs | 117 ++- .../Events/EventActions/MissionStateAction.cs | 66 ++ .../EventActions/ModifyLocationAction.cs | 91 ++ .../EventActions/NPCChangeTeamAction.cs | 33 +- .../Events/EventActions/NPCFollowAction.cs | 5 +- .../Events/EventActions/NPCWaitAction.cs | 31 +- .../Events/EventActions/RemoveItemAction.cs | 21 +- .../Events/EventActions/ReputationAction.cs | 50 +- .../Events/EventActions/SpawnAction.cs | 77 +- .../Events/EventActions/StatusEffectAction.cs | 36 +- .../Events/EventActions/TagAction.cs | 12 +- .../Events/EventActions/TriggerEventAction.cs | 26 +- .../Events/EventActions/WaitAction.cs | 3 - .../SharedSource/Events/EventManager.cs | 273 +++-- .../SharedSource/Events/EventPrefab.cs | 19 +- .../SharedSource/Events/EventSet.cs | 55 +- .../Missions/AbandonedOutpostMission.cs | 64 +- .../Events/Missions/AlienRuinMission.cs | 10 +- .../Events/Missions/BeaconMission.cs | 17 +- .../Events/Missions/EndMission.cs | 295 ++++++ .../Events/Missions/EscortMission.cs | 40 +- .../Events/Missions/GoToMission.cs | 6 +- .../Events/Missions/MineralMission.cs | 52 +- .../SharedSource/Events/Missions/Mission.cs | 126 ++- .../Events/Missions/MissionPrefab.cs | 45 +- .../Events/Missions/MonsterMission.cs | 9 +- .../Events/Missions/NestMission.cs | 26 +- .../Events/Missions/PirateMission.cs | 85 +- .../Events/Missions/SalvageMission.cs | 549 ++++++---- .../Events/Missions/ScanMission.cs | 19 +- .../SharedSource/Events/MonsterEvent.cs | 7 +- .../SharedSource/Events/ScriptedEvent.cs | 4 +- .../Extensions/IEnumerableExtensions.cs | 19 +- .../Extensions/StringExtensions.cs | 7 + .../GameSession/AutoItemPlacer.cs | 2 + .../SharedSource/GameSession/CargoManager.cs | 48 +- .../SharedSource/GameSession/CrewManager.cs | 38 +- .../GameSession/Data/CampaignMetadata.cs | 13 +- .../SharedSource/GameSession/Data/Factions.cs | 130 ++- .../GameSession/Data/Reputation.cs | 55 +- .../SharedSource/GameSession/Data/Wallet.cs | 42 +- .../GameSession/GameModes/CampaignMode.cs | 404 ++++++-- .../GameSession/GameModes/CampaignSettings.cs | 10 +- .../GameModes/MultiPlayerCampaign.cs | 26 +- .../SharedSource/GameSession/GameSession.cs | 86 +- .../SharedSource/GameSession/HireManager.cs | 20 +- .../SharedSource/GameSession/MedicalClinic.cs | 40 +- .../GameSession/SlideshowPrefab.cs | 60 ++ .../SharedSource/Items/CharacterInventory.cs | 35 +- .../Items/Components/DockingPort.cs | 8 +- .../SharedSource/Items/Components/Door.cs | 73 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Components/EntitySpawnerComponent.cs | 46 +- .../Items/Components/Holdable/Holdable.cs | 13 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/Holdable/MeleeWeapon.cs | 2 +- .../Items/Components/Holdable/RangedWeapon.cs | 68 +- .../Items/Components/Holdable/RepairTool.cs | 22 +- .../Items/Components/Holdable/Throwable.cs | 5 + .../Items/Components/ItemComponent.cs | 28 +- .../Items/Components/ItemContainer.cs | 137 +-- .../Items/Components/Machines/Engine.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 2 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Machines/Sonar.cs | 2 +- .../Items/Components/Machines/Steering.cs | 4 +- .../Items/Components/Power/PowerContainer.cs | 2 +- .../Items/Components/Projectile.cs | 110 ++- .../Items/Components/Repairable.cs | 57 +- .../Items/Components/Signal/Connection.cs | 34 +- .../Components/Signal/ConnectionPanel.cs | 4 +- .../Components/Signal/CustomInterface.cs | 18 +- .../Items/Components/Signal/LightComponent.cs | 42 +- .../Items/Components/Signal/MotionSensor.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 23 +- .../Items/Components/Signal/Wire.cs | 167 ++-- .../Items/Components/TriggerComponent.cs | 28 +- .../SharedSource/Items/Components/Turret.cs | 417 +++++--- .../SharedSource/Items/Components/Wearable.cs | 11 +- .../SharedSource/Items/Inventory.cs | 13 +- .../SharedSource/Items/Item.cs | 105 +- .../SharedSource/Items/ItemEventData.cs | 7 + .../SharedSource/Items/ItemPrefab.cs | 105 +- .../SharedSource/Items/RelatedItem.cs | 333 ++++--- .../SharedSource/Map/Entity.cs | 4 + .../SharedSource/Map/Explosion.cs | 18 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 118 ++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 2 +- .../SharedSource/Map/IDamageable.cs | 2 +- .../SharedSource/Map/Levels/Biome.cs | 6 + .../Map/Levels/CaveGenerationParams.cs | 8 +- .../SharedSource/Map/Levels/Level.cs | 287 ++++-- .../SharedSource/Map/Levels/LevelData.cs | 89 +- .../Map/Levels/LevelGenerationParams.cs | 139 ++- .../Levels/LevelObjects/LevelObjectManager.cs | 59 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 6 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 24 +- .../SharedSource/Map/LinkedSubmarine.cs | 2 +- .../SharedSource/Map/Map/Location.cs | 294 ++++-- .../SharedSource/Map/Map/LocationType.cs | 88 +- .../Map/Map/LocationTypeChange.cs | 104 +- .../SharedSource/Map/Map/Map.cs | 518 +++++++--- .../SharedSource/Map/MapEntity.cs | 72 +- .../Map/Outposts/BeaconStationInfo.cs | 3 + .../SharedSource/Map/Outposts/NPCSet.cs | 14 +- .../Map/Outposts/OutpostGenerationParams.cs | 115 ++- .../Map/Outposts/OutpostGenerator.cs | 159 ++- .../Map/Outposts/OutpostModuleInfo.cs | 2 - .../SharedSource/Map/PriceInfo.cs | 24 +- .../SharedSource/Map/Structure.cs | 53 +- .../SharedSource/Map/StructurePrefab.cs | 38 +- .../SharedSource/Map/Submarine.cs | 229 +++-- .../SharedSource/Map/SubmarineBody.cs | 226 +++-- .../SharedSource/Map/SubmarineInfo.cs | 41 +- .../SharedSource/Map/WayPoint.cs | 52 +- .../SharedSource/Networking/Client.cs | 6 + .../Networking/ClientPermissions.cs | 18 +- .../Networking/INetSerializableStruct.cs | 9 +- .../Networking/OrderChatMessage.cs | 4 +- .../SharedSource/Networking/ServerLog.cs | 3 + .../SharedSource/Networking/ServerSettings.cs | 62 +- .../SharedSource/Networking/Voting.cs | 1 - .../SharedSource/Physics/PhysicsBody.cs | 78 +- .../Prefabs/IImplementsVariants.cs | 24 +- .../SharedSource/Prefabs/PrefabCollection.cs | 111 ++- .../SharedSource/ProcGen/VoronoiElements.cs | 19 +- .../SharedSource/Screens/GameScreen.cs | 10 +- .../Serialization/SerializableProperty.cs | 56 +- .../Serialization/XMLExtensions.cs | 45 +- .../SharedSource/Settings/CreatureMetrics.cs | 133 ++- .../SharedSource/Settings/GameSettings.cs | 11 +- .../Settings/ServerLanguageOptions.cs | 63 ++ .../SharedSource/Sprite/ConditionalSprite.cs | 21 +- .../StatusEffects/DelayedEffect.cs | 12 +- .../StatusEffects/PropertyConditional.cs | 725 ++++++++------ .../StatusEffects/StatusEffect.cs | 935 +++++++++++------- .../SharedSource/Steam/SteamManager.cs | 3 +- .../SharedSource/Steam/Workshop.cs | 10 +- .../SharedSource/SteamAchievementManager.cs | 22 +- .../Text/LocalizedString/ConcatLString.cs | 1 + .../BarotraumaShared/SharedSource/Timing.cs | 3 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 60 +- .../SharedSource/Utils/CrossThread.cs | 2 +- .../SharedSource/Utils/MathUtils.cs | 60 +- .../SharedSource/Utils/Md5Hash.cs | 50 +- .../SharedSource/Utils/Option/None.cs | 17 - .../SharedSource/Utils/Option/Option.cs | 125 ++- .../SharedSource/Utils/Option/Some.cs | 25 - .../SharedSource/Utils/ReflectionUtils.cs | 6 +- .../Utils/SerializableDateTime.cs | 5 +- .../SharedSource/Utils/ToolBox.cs | 3 +- Barotrauma/BarotraumaShared/changelog.txt | 416 ++++++++ .../BarotraumaTest/EndpointParseTests.cs | 15 +- Barotrauma/BarotraumaTest/MathUtilsTests.cs | 67 ++ .../PropertyConditionalTests.cs | 67 ++ .../SerializableDateTimeTests.cs | 25 +- .../Callbacks/CallResult.cs | 6 +- .../Classes/AuthTicket.cs | 4 +- .../Facepunch.Steamworks/Classes/Dispatch.cs | 6 +- .../Facepunch.Steamworks.Posix.csproj | 7 +- .../Facepunch.Steamworks.Win64.csproj | 5 + .../Generated/Interfaces/ISteamApps.cs | 2 +- .../Generated/Interfaces/ISteamInventory.cs | 16 +- .../Networking/Connection.cs | 20 +- .../Networking/ConnectionManager.cs | 4 +- .../Networking/NetIdentity.cs | 5 +- .../Networking/NetPingLocation.cs | 9 +- .../Facepunch.Steamworks/Networking/Socket.cs | 6 +- .../Networking/SocketManager.cs | 12 +- .../Facepunch.Steamworks/ServerList/Base.cs | 20 +- .../ServerList/Favourites.cs | 1 + .../ServerList/Friends.cs | 1 + .../ServerList/History.cs | 1 + .../ServerList/Internet.cs | 2 +- .../ServerList/LocalNetwork.cs | 1 + Libraries/Facepunch.Steamworks/SteamApps.cs | 66 +- Libraries/Facepunch.Steamworks/SteamClient.cs | 8 +- .../Facepunch.Steamworks/SteamFriends.cs | 82 +- Libraries/Facepunch.Steamworks/SteamInput.cs | 16 +- .../Facepunch.Steamworks/SteamInventory.cs | 50 +- .../Facepunch.Steamworks/SteamMatchmaking.cs | 36 +- .../SteamMatchmakingServers.cs | 2 +- Libraries/Facepunch.Steamworks/SteamMusic.cs | 24 +- .../Facepunch.Steamworks/SteamNetworking.cs | 26 +- .../SteamNetworkingSockets.cs | 36 +- .../SteamNetworkingUtils.cs | 28 +- .../Facepunch.Steamworks/SteamParental.cs | 16 +- .../Facepunch.Steamworks/SteamParties.cs | 10 +- .../Facepunch.Steamworks/SteamRemotePlay.cs | 12 +- .../SteamRemoteStorage.cs | 38 +- .../Facepunch.Steamworks/SteamScreenshots.cs | 18 +- Libraries/Facepunch.Steamworks/SteamServer.cs | 70 +- .../Facepunch.Steamworks/SteamServerStats.cs | 18 +- Libraries/Facepunch.Steamworks/SteamUgc.cs | 23 +- Libraries/Facepunch.Steamworks/SteamUser.cs | 88 +- .../Facepunch.Steamworks/SteamUserStats.cs | 38 +- Libraries/Facepunch.Steamworks/SteamUtils.cs | 58 +- Libraries/Facepunch.Steamworks/SteamVideo.cs | 10 +- .../SteamMatchmakingResponses.cs | 23 +- .../Structs/Achievement.cs | 15 +- .../Facepunch.Steamworks/Structs/Clan.cs | 18 +- .../Structs/Controller.cs | 14 +- .../Facepunch.Steamworks/Structs/Friend.cs | 33 +- .../Structs/InventoryDef.cs | 56 +- .../Structs/InventoryItem.cs | 18 +- .../Structs/InventoryRecipe.cs | 2 +- .../Structs/InventoryResult.cs | 16 +- .../Structs/Leaderboard.cs | 32 +- .../Structs/LeaderboardEntry.cs | 2 +- .../Facepunch.Steamworks/Structs/Lobby.cs | 54 +- .../Structs/LobbyQuery.cs | 6 +- .../Structs/PartyBeacon.cs | 19 +- .../Structs/RemotePlaySession.cs | 6 +- .../Structs/Screenshot.cs | 6 +- .../Facepunch.Steamworks/Structs/Server.cs | 28 +- .../Structs/ServerInit.cs | 2 +- .../Facepunch.Steamworks/Structs/Stat.cs | 32 +- .../Facepunch.Steamworks/Structs/UgcEditor.cs | 25 +- .../Facepunch.Steamworks/Structs/UgcItem.cs | 48 +- .../Facepunch.Steamworks/Structs/UgcQuery.cs | 42 +- .../Structs/UgcResultPage.cs | 5 +- .../Utility/SourceServerQuery.cs | 6 +- .../Utility/SteamInterface.cs | 10 +- .../Utility/Utf8String.cs | 8 +- .../Facepunch.Steamworks/Utility/Utility.cs | 4 +- 483 files changed, 17487 insertions(+), 8548 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs create mode 100644 Barotrauma/BarotraumaShared/Data/languageoptions.xml create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs create mode 100644 Barotrauma/BarotraumaTest/MathUtilsTests.cs create mode 100644 Barotrauma/BarotraumaTest/PropertyConditionalTests.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 839d52d50..187b87f7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -36,8 +36,8 @@ namespace Barotrauma private float minZoom = 0.1f; public float MinZoom { - get { return minZoom;} - set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } + get { return minZoom; } + set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } } private float maxZoom = 2.0f; @@ -63,7 +63,7 @@ namespace Barotrauma private float prevZoom; public float Shake; - private Vector2 shakePosition; + public Vector2 ShakePosition { get; private set; } private float shakeTimer; private float globalZoomScale = 1.0f; @@ -371,7 +371,7 @@ namespace Barotrauma if (Shake < 0.01f) { - shakePosition = Vector2.Zero; + ShakePosition = Vector2.Zero; shakeTimer = 0.0f; } else @@ -379,11 +379,11 @@ namespace Barotrauma shakeTimer += deltaTime * 5.0f; Vector2 noisePos = new Vector2((float)PerlinNoise.CalculatePerlin(shakeTimer, shakeTimer, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(shakeTimer, shakeTimer, 0.5f) - 0.5f); - shakePosition = noisePos * Shake * 2.0f; + ShakePosition = noisePos * Shake * 2.0f; Shake = MathHelper.Lerp(Shake, 0.0f, deltaTime * 2.0f); } - Translate(moveCam + shakePosition); + Translate(moveCam + ShakePosition); Freeze = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index b1392f9cb..7adc4fb99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -6,6 +6,8 @@ namespace Barotrauma { class CameraTransition { + private static List activeTransitions = new List(); + public bool Running { get; @@ -19,6 +21,7 @@ namespace Barotrauma private readonly float? endZoom; public readonly float WaitDuration; + public float EndWaitDuration = 0.1f; public readonly float PanDuration; public readonly bool FadeOut; public readonly bool LosFadeIn; @@ -29,6 +32,8 @@ namespace Barotrauma public bool AllowInterrupt = false; public bool RemoveControlFromCharacter = true; + + public bool RunWhilePaused = true; public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null) { @@ -45,8 +50,19 @@ namespace Barotrauma if (targetEntity == null) { return; } Running = true; - CoroutineManager.StopCoroutines("CameraTransition"); + + prevControlled = Character.Controlled; + activeTransitions.RemoveAll(a => !CoroutineManager.IsCoroutineRunning(a.updateCoroutine)); + foreach (var activeTransition in activeTransitions) + { + if (activeTransition.prevControlled != null) + { + prevControlled ??= activeTransition.prevControlled; + } + activeTransition.Stop(); + } updateCoroutine = CoroutineManager.StartCoroutine(Update(targetEntity, cam), "CameraTransition"); + activeTransitions.Add(this); } public void Stop() @@ -62,11 +78,13 @@ namespace Barotrauma #endif } + private float DeltaTime => CoroutineManager.Paused && !RunWhilePaused ? 0 : CoroutineManager.DeltaTime; + private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) { if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; } - prevControlled = Character.Controlled; + prevControlled ??= Character.Controlled; if (RemoveControlFromCharacter) { #if CLIENT @@ -80,6 +98,7 @@ namespace Barotrauma float endZoom = this.endZoom ?? 0.5f; Vector2 initialCameraPos = cam.Position; Vector2? initialTargetPos = targetEntity?.WorldPosition; + Vector2 endPos = cam.Position; float timer = -WaitDuration; @@ -137,13 +156,13 @@ namespace Barotrauma { startPos += targetEntity.WorldPosition - initialTargetPos.Value; } - Vector2 endPos = cameraEndPos.HasValue ? + endPos = cameraEndPos.HasValue ? new Vector2( MathHelper.Lerp(minPos.X, maxPos.X, (cameraEndPos.Value.ToVector2().X + 1.0f) / 2.0f), MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) : prevControlled?.WorldPosition ?? targetEntity.WorldPosition; - Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration); + Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration) + cam.ShakePosition; cam.Translate(cameraPos - cam.Position); #if CLIENT @@ -162,14 +181,21 @@ namespace Barotrauma Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); } #endif - timer += CoroutineManager.DeltaTime; + timer += DeltaTime; yield return CoroutineStatus.Running; } - Running = false; + float endTimer = 0.0f; + while (endTimer <= EndWaitDuration) + { + cam.Translate(endPos - cam.Position); + cam.Zoom = endZoom; + endTimer += DeltaTime; + yield return CoroutineStatus.Running; + } - yield return new WaitForSeconds(0.1f); + Running = false; #if CLIENT GUI.ScreenOverlayColor = Color.TransparentBlack; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index cf8c7c628..44032309e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -19,6 +19,17 @@ namespace Barotrauma private IEnumerable FadeOutColors(float time) { + + Dictionary originalColors = new Dictionary(); + foreach (var item in thalamusItems) + { + originalColors.Add(item, item.SpriteColor); + } + foreach (var structure in thalamusStructures) + { + originalColors.Add(structure, structure.SpriteColor); + } + float timer = 0; while (timer < time) { @@ -26,15 +37,16 @@ namespace Barotrauma float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer)); foreach (var item in thalamusItems) { + if (item.Color.A == 0) { continue; } if (item.Prefab.BrokenSprites.None()) { - Color c = item.Prefab.SpriteColor; + Color c = originalColors[item]; item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } } foreach (var structure in thalamusStructures) { - Color c = structure.Prefab.SpriteColor; + Color c = originalColors[structure]; structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index a9fa83ac4..6f3eda991 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -99,12 +99,14 @@ namespace Barotrauma float newAngularVelocity = Collider.AngularVelocity; Collider.CorrectPosition(character.MemState, out newPosition, out newVelocity, out newRotation, out newAngularVelocity); - newVelocity = newVelocity.ClampLength(100.0f); - if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } - overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; - - Collider.LinearVelocity = newVelocity; - Collider.AngularVelocity = newAngularVelocity; + if (Collider.BodyType == BodyType.Dynamic) + { + newVelocity = newVelocity.ClampLength(100.0f); + if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } + overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; + Collider.LinearVelocity = newVelocity; + Collider.AngularVelocity = newAngularVelocity; + } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index d20050fe4..0f86bcc9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -109,6 +109,26 @@ namespace Barotrauma set => grainStrength = Math.Max(0, value); } + /// + /// Can be used by status effects + /// + public float CollapseEffectStrength + { + get { return Level.Loaded?.Renderer?.CollapseEffectStrength ?? 0.0f; } + set + { + if (Level.Loaded?.Renderer == null) { return; } + if (Controlled == this) + { + float strength = MathHelper.Clamp(value, 0.0f, 1.0f); + Level.Loaded.Renderer.CollapseEffectStrength = strength; + Level.Loaded.Renderer.CollapseEffectOrigin = Submarine?.WorldPosition ?? WorldPosition; + Screen.Selected.Cam.Shake = Math.Max(MathF.Pow(strength, 3) * 100.0f, Screen.Selected.Cam.Shake); + Screen.Selected.Cam.Rotation = strength * (PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, (float)Timing.TotalTime * 0.05f) - 0.5f); + Level.Loaded.Renderer.ChromaticAberrationStrength = value * 50.0f; + } + } + } /// /// Can be used to set camera shake from status effects /// @@ -278,6 +298,21 @@ namespace Barotrauma { keys[i].SetState(); } + + if (CharacterInventory.IsMouseOnInventory && CharacterHUD.ShouldDrawInventory(this)) + { + ResetInputIfPrimaryMouse(InputType.Use); + ResetInputIfPrimaryMouse(InputType.Shoot); + ResetInputIfPrimaryMouse(InputType.Select); + void ResetInputIfPrimaryMouse(InputType inputType) + { + if (GameSettings.CurrentConfig.KeyMap.Bindings[inputType].MouseButton == MouseButton.PrimaryMouse) + { + keys[(int)inputType].Reset(); + } + } + } + //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) @@ -296,8 +331,7 @@ namespace Barotrauma float targetOffsetAmount = 0.0f; if (moveCam) { - if (NeedsAir && !IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) @@ -911,7 +945,7 @@ namespace Barotrauma { name += " " + TextManager.Get("Disguised"); } - else if (Info.Title != null) + else if (Info.Title != null && TeamID != CharacterTeamType.Team1) { name += '\n' + Info.Title; } @@ -987,13 +1021,13 @@ namespace Barotrauma } } - if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) + if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), - CharacterHealth.DisplayedVitality / MaxVitality, + CharacterHealth.DisplayedVitality / MaxVitality, Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 80b153e28..83c669f6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -13,9 +13,8 @@ namespace Barotrauma { const float BossHealthBarDuration = 120.0f; - class BossHealthBar + abstract class BossProgressBar { - public readonly Character Character; public float FadeTimer; public readonly GUIComponent TopContainer; @@ -24,9 +23,18 @@ namespace Barotrauma public readonly GUIProgressBar TopHealthBar; public readonly GUIProgressBar SideHealthBar; - public BossHealthBar(Character character) + public abstract bool Completed { get; } + + public abstract bool Interrupted { get; } + + public abstract float State { get; } + + public abstract string NumberToDisplay { get; } + + public abstract Color Color { get; } + + public BossProgressBar(LocalizedString label) { - Character = character; FadeTimer = BossHealthBarDuration; TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter) @@ -34,7 +42,7 @@ namespace Barotrauma MinSize = new Point(100, 50), RelativeOffset = new Vector2(0.0f, 0.01f) }, isHorizontal: false, childAnchor: Anchor.TopCenter); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), label, textAlignment: Alignment.Center, textColor: GUIStyle.Red); TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) { MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) @@ -42,22 +50,95 @@ namespace Barotrauma { Color = GUIStyle.Red }; + CreateNumberText(TopHealthBar); SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { MinSize = new Point(80, 60) }, isHorizontal: false, childAnchor: Anchor.TopRight); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), label, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") { Color = GUIStyle.Red }; + CreateNumberText(SideHealthBar); TopContainer.Visible = SideContainer.Visible = false; TopContainer.CanBeFocused = false; TopContainer.Children.ForEach(c => c.CanBeFocused = false); SideContainer.CanBeFocused = false; SideContainer.Children.ForEach(c => c.CanBeFocused = false); + + void CreateNumberText(GUIComponent parent) + { + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform) + { AbsoluteOffset = new Point(2) }, + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorDark) + { + TextGetter = () => NumberToDisplay + }; + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform), + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorBright) + { + TextGetter = () => NumberToDisplay + }; + } + } + + public abstract bool IsDuplicate(object targetObject); + } + + class BossHealthBar : BossProgressBar + { + public readonly Character Character; + + public override float State => Character.Vitality / Character.MaxVitality; + + public override bool Completed => Character.IsDead; + + public override bool Interrupted => Character.Removed || !Character.Enabled; + + public override Color Color => + Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.ParalysisType) > 0 ? + GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; + + public override string NumberToDisplay => string.Empty; + + public BossHealthBar(Character character) : base(character.DisplayName) + { + Character = character; + } + + public override bool IsDuplicate(object targetObject) + { + return targetObject is Character character && Character == character; + } + } + + class MissionProgressBar : BossProgressBar + { + public readonly Mission Mission; + + public override float State => Mission.State / (float)Mission.Prefab.MaxProgressState; + + public override bool Completed => Mission.State >= Mission.Prefab.MaxProgressState; + + public override bool Interrupted => Mission.Failed || GameMain.GameSession?.Missions == null || !GameMain.GameSession.Missions.Contains(Mission); + + public override Color Color => GUIStyle.Red; + + public override string NumberToDisplay => Mission.Prefab.ShowProgressInNumbers ? + $"{Mission.State}/{Mission.Prefab.MaxProgressState}" : + string.Empty; + + public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel) + { + Mission = mission; + } + + public override bool IsDuplicate(object targetObject) + { + return targetObject is Mission mission && Mission == mission; } } @@ -69,9 +150,10 @@ namespace Barotrauma private static readonly List brokenItems = new List(); private static float brokenItemsCheckTimer; - private static readonly List bossHealthBars = new List(); + private static readonly List bossProgressBars = new List(); private static readonly Dictionary cachedHudTexts = new Dictionary(); + private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None; private static GUILayoutGroup bossHealthContainer; @@ -107,7 +189,7 @@ namespace Barotrauma GameMain.GameSession?.Campaign != null && (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI); - private static bool ShouldDrawInventory(Character character) + public static bool ShouldDrawInventory(Character character) { var controller = character.SelectedItem?.GetComponent(); @@ -121,10 +203,15 @@ namespace Barotrauma public static LocalizedString GetCachedHudText(string textTag, InputType keyBind) { + if (cachedHudTextLanguage != GameSettings.CurrentConfig.Language) + { + cachedHudTexts.Clear(); + } Identifier key = (textTag + keyBind).ToIdentifier(); if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; cachedHudTexts.Add(key, text); + cachedHudTextLanguage = GameSettings.CurrentConfig.Language; return text; } @@ -159,7 +246,7 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { - UpdateBossHealthBars(deltaTime); + UpdateBossProgressBars(deltaTime); if (GUI.DisableHUD) { @@ -623,7 +710,9 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.Y += GUIStyle.SubHeadingFont.MeasureString(focusName).Y; - if (character.FocusedCharacter.Info?.Title != null && !character.FocusedCharacter.Info.Title.IsNullOrEmpty()) + if (character.FocusedCharacter.Info?.Title != null && + !character.FocusedCharacter.Info.Title.IsNullOrEmpty() && + character.FocusedCharacter.TeamID != CharacterTeamType.Team1) { GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.Info.Title, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.Y += GUIStyle.SubHeadingFont.MeasureString(character.FocusedCharacter.Info.Title.Value).Y; @@ -648,7 +737,8 @@ namespace Barotrauma if (!character.DisableHealthWindow && character.IsFriendly(character.FocusedCharacter) && character.FocusedCharacter.CharacterHealth.UseHealthWindow && - character.CanInteractWith(character.FocusedCharacter, 160f, false)) + character.CanInteractWith(character.FocusedCharacter, 160f, false) && + !character.IsClimbing) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health), GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); @@ -661,27 +751,47 @@ namespace Barotrauma } } - public static void ShowBossHealthBar(Character character) + public static void ShowBossHealthBar(Character character, float damage) { if (character == null || character.IsDead || character.Removed) { return; } + if (bossProgressBars.Any(b => b.IsDuplicate(character))) { return; } + AddBossProgressBar(new BossHealthBar(character)); + } + public static void ShowMissionProgressBar(Mission mission) + { + if (mission == null || mission.Completed || mission.Failed) { return; } + if (bossProgressBars.Any(b => b.IsDuplicate(mission))) { return; } + AddBossProgressBar(new MissionProgressBar(mission)); + } + + public static void ClearBossProgressBars() + { + for (int i = bossProgressBars.Count - 1; i>= 0; i--) + { + RemoveBossProgressBar(bossProgressBars[i]); + } + bossProgressBars.Clear(); + } + + private static void RemoveBossProgressBar(BossProgressBar progressBar) + { + progressBar.SideContainer.Parent?.RemoveChild(progressBar.SideContainer); + progressBar.TopContainer.Parent?.RemoveChild(progressBar.TopContainer); + bossProgressBars.Remove(progressBar); + } + + private static void AddBossProgressBar(BossProgressBar progressBar) + { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; if (healthBarMode == EnemyHealthBarMode.HideAll) { return; } - - var existingBar = bossHealthBars.Find(b => b.Character == character); - if (existingBar != null) + if (bossProgressBars.Count > 5) { - existingBar.FadeTimer = BossHealthBarDuration; - return; - } - - if (bossHealthBars.Count > 5) - { - BossHealthBar oldestHealthBar = bossHealthBars.First(); - foreach (var bar in bossHealthBars) + BossProgressBar oldestHealthBar = bossProgressBars.First(); + foreach (var bar in bossProgressBars) { if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize) { @@ -690,62 +800,69 @@ namespace Barotrauma } oldestHealthBar.FadeTimer = Math.Min(oldestHealthBar.FadeTimer, 1.0f); } - - bossHealthBars.Add(new BossHealthBar(character)); + bossProgressBars.Add(progressBar); } - public static void UpdateBossHealthBars(float deltaTime) + public static void UpdateBossProgressBars(float deltaTime) { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; - for (int i = 0; i < bossHealthBars.Count; i++) + for (int i = 0; i < bossProgressBars.Count; i++) { - var bossHealthBar = bossHealthBars[i]; + var bossHealthBar = bossProgressBars[i]; - bool showTopBar = i == 0; - if (showTopBar != bossHealthBar.TopContainer.Visible) + bool showTopBar = i == bossProgressBars.Count - 1; + if (showTopBar && !bossHealthBar.TopContainer.Visible) { - bossHealthContainer.Recalculate(); + bossHealthBar.SideContainer.SetAsLastChild(); + SetColor(bossHealthBar, bossHealthBar.SideContainer, 0); } bossHealthBar.TopContainer.Visible = showTopBar; bossHealthBar.SideContainer.Visible = !bossHealthBar.TopContainer.Visible; - float health = bossHealthBar.Character.Vitality / bossHealthBar.Character.MaxVitality; - + bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = bossHealthBar.State; float alpha = Math.Min(bossHealthBar.FadeTimer, 1.0f); - foreach (var c in bossHealthBar.SideContainer.GetAllChildren().Concat(bossHealthBar.TopContainer.GetAllChildren())) + + if (bossHealthBar.TopContainer.Visible) { - c.Color = new Color(c.Color, (byte)(alpha * 255)); - if (c is GUITextBlock textBlock) + SetColor(bossHealthBar, bossHealthBar.TopContainer, alpha); + } + if (bossHealthBar.SideContainer.Visible) + { + SetColor(bossHealthBar, bossHealthBar.SideContainer, alpha); + } + + static void SetColor(BossProgressBar bossHealthBar, GUIComponent container, float alpha) + { + foreach (var component in container.GetAllChildren()) { - textBlock.TextColor = new Color(bossHealthBar.Character.IsDead ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + component.Color = new Color(bossHealthBar.Color, (byte)(alpha * 255)); + if (component is GUITextBlock textBlock) + { + textBlock.TextColor = new Color(bossHealthBar.Completed ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + } } } - bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; - Color color = bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("poison") > 0 || bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("paralysis") > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; - bossHealthBar.TopHealthBar.Color = bossHealthBar.SideHealthBar.Color = color; - - if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) + if (bossHealthBar.Interrupted) { bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 1.0f); } - else if (bossHealthBar.Character.IsDead) + else if (bossHealthBar.Completed) { bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 5.0f); } bossHealthBar.FadeTimer -= deltaTime; } - - for (int i = bossHealthBars.Count - 1; i >= 0 ; i--) + for (int i = bossProgressBars.Count - 1; i >= 0 ; i--) { - var bossHealthBar = bossHealthBars[i]; + var bossHealthBar = bossProgressBars[i]; if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll) { bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); - bossHealthBars.RemoveAt(i); + bossProgressBars.RemoveAt(i); bossHealthContainer.Recalculate(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index c4bdcaba9..47d87e1ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -540,37 +540,45 @@ namespace Barotrauma string ragdollFile = inc.ReadString(); Identifier npcId = inc.ReadIdentifier(); + + Identifier factionId = inc.ReadIdentifier(); + float minReputationToHire = 0.0f; + if (factionId != default) + { + minReputationToHire = inc.ReadSingle(); + } + uint jobIdentifier = inc.ReadUInt32(); int variant = inc.ReadByte(); - JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (jobIdentifier > 0) - { + { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); + if (jobPrefab == null) + { + 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)) { float skillLevel = inc.ReadSingle(); skillLevels.Add(skillPrefab.Identifier, skillLevel); - } - } + } + } - // TODO: animations CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) { - ID = infoID + ID = infoID, + MinReputationToHire = (factionId, minReputationToHire) }; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; ch.Head.HairColor = hairColor; ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); - if (ch.Job != null) - { - ch.Job.OverrideSkills(skillLevels); - } + ch.Job?.OverrideSkills(skillLevels); - ch.ExperiencePoints = inc.ReadUInt16(); + ch.ExperiencePoints = inc.ReadInt32(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); return ch; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 23db24f69..8139f283c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -593,6 +593,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.Faction = inc.ReadIdentifier(); character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 6fe86291d..107816307 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -87,6 +87,8 @@ namespace Barotrauma /// Container for the icons above the health bar /// private GUIComponent afflictionIconContainer; + private float afflictionIconRefreshTimer; + const float AfflictionIconRefreshInterval = 1.0f; private GUIButton showHiddenAfflictionsButton; @@ -699,10 +701,11 @@ namespace Barotrauma blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + float afflictionGrainStrength = affliction.GetScreenGrainStrength(); if (afflictionGrainStrength > 0.0f) { - grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); + grainStrength = Math.Max(grainStrength, afflictionGrainStrength); Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } @@ -861,7 +864,7 @@ namespace Barotrauma { treatmentButton.ToolTip = RichString.Rich( - $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] " + $"‖color:gui.green‖[{PlayerInput.PrimaryMouseLabel}] " + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + treatmentButton.ToolTip.NestedStr); } @@ -1018,12 +1021,8 @@ namespace Barotrauma foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; - if (affliction.Prefab.AfflictionOverlay != null) - { - Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; - ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); - } + affliction.Prefab.AfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, + new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); } float damageOverlayAlpha = DamageOverlayTimer; @@ -1157,15 +1156,20 @@ namespace Barotrauma } } - afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + afflictionIconRefreshTimer -= deltaTime; + if (afflictionIconRefreshTimer <= 0.0f) { - if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } - if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } - var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); - var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); - return index1.CompareTo(index2); - }); - (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + { + if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } + if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } + var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); + var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); + return index1.CompareTo(index2); + }); + (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconRefreshTimer = AfflictionIconRefreshInterval; + } Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) @@ -1983,6 +1987,7 @@ namespace Barotrauma { newAfflictions.Clear(); newPeriodicEffects.Clear(); + bool newAdded = false; byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -2062,6 +2067,7 @@ namespace Barotrauma { existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction, limb); + newAdded = true; } existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) @@ -2088,6 +2094,11 @@ namespace Barotrauma CalculateVitality(); DisplayedVitality = Vitality; + + if (newAdded) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } partial void UpdateSkinTint() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 5fe12e73e..1acc8736a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -554,7 +554,7 @@ namespace Barotrauma float damage = 0; foreach (var affliction in result.Afflictions) { - if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { damage += affliction.GetVitalityDecrease(null); } @@ -563,11 +563,11 @@ namespace Barotrauma float bleedingDamageMultiplier = 1; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { - if (damageModifier.MatchesAfflictionType("damage")) + if (damageModifier.MatchesAfflictionType(AfflictionPrefab.DamageType)) { damageMultiplier *= damageModifier.DamageMultiplier; } - else if (damageModifier.MatchesAfflictionType("bleeding")) + else if (damageModifier.MatchesAfflictionType(AfflictionPrefab.BleedingType)) { bleedingDamageMultiplier *= damageModifier.DamageMultiplier; } @@ -599,7 +599,7 @@ namespace Barotrauma { if (damageModifier.DamageMultiplier > 0 && !string.IsNullOrWhiteSpace(damageModifier.DamageParticle)) { - overrideParticle = GameMain.ParticleManager?.FindPrefab(damageModifier.DamageParticle); + overrideParticle = ParticleManager.FindPrefab(damageModifier.DamageParticle); break; } } @@ -646,7 +646,7 @@ namespace Barotrauma dripParticleTimer += wetTimer * deltaTime * Mass * (wetTimer > 0.9f ? 50.0f : 5.0f); if (dripParticleTimer > 1.0f) { - float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.width, body.height) : body.radius; + float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.Width, body.Height) : body.Radius; GameMain.ParticleManager.CreateParticle( "waterdrop", WorldPosition + Rand.Vector(Rand.Range(0.0f, ConvertUnits.ToDisplayUnits(dropRadius))), @@ -683,10 +683,10 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { - float brightness = Math.Max(1.0f - burnOverLayStrength, 0.2f); var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } - + float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; + float brightness = Math.Max(1.0f - burn, 0.2f); Color clr = spriteParams.Color; if (!spriteParams.IgnoreTint) { @@ -727,7 +727,7 @@ namespace Barotrauma } } - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || @@ -1245,6 +1245,12 @@ namespace Barotrauma }; paramsToPass.Params["wearableUvToClipperUv"] = wearableUvToClipperUv; + paramsToPass.Params["stencilUVmin"] = new Vector2( + (float)alphaClipper.Sprite.SourceRect.X / alphaClipper.Sprite.Texture.Width, + (float)alphaClipper.Sprite.SourceRect.Y / alphaClipper.Sprite.Texture.Height); + paramsToPass.Params["stencilUVmax"] = new Vector2( + (float)alphaClipper.Sprite.SourceRect.Right / alphaClipper.Sprite.Texture.Width, + (float)alphaClipper.Sprite.SourceRect.Bottom / alphaClipper.Sprite.Texture.Height); paramsToPass.Params["clipperTexelSize"] = 2f / alphaClipper.Sprite.Texture.Width; paramsToPass.Params["aCutoff"] = 2f / 255f; paramsToPass.Params["xTexture"] = wearable.Sprite.Texture; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 19d98e5e7..1fe714a9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -425,6 +425,10 @@ namespace Barotrauma { CheatsEnabled = true; SteamAchievementManager.CheatsEnabled = true; + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + campaign.CheatsEnabled = true; + } NewMessage("Enabled cheat commands.", Color.Red); #if USE_STEAM NewMessage("Steam achievements have been disabled during this play session.", Color.Red); @@ -639,7 +643,7 @@ namespace Barotrauma { if (Submarine.MainSub == null) { return; } MapEntity.SelectedList.Clear(); - MapEntity.mapEntityList.ForEach(me => me.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); WikiImage.Create(Submarine.MainSub); })); @@ -752,7 +756,7 @@ namespace Barotrauma state = !GameMain.LightManager.LosEnabled; } GameMain.LightManager.LosEnabled = state; - NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("los", false); @@ -763,7 +767,7 @@ namespace Barotrauma state = !GameMain.LightManager.LightingEnabled; } GameMain.LightManager.LightingEnabled = state; - NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("lighting|lights", false); @@ -781,7 +785,7 @@ namespace Barotrauma hull.OriginalAmbientLight = null; } } - NewMessage("Restored all hull ambient lights", Color.White); + NewMessage("Restored all hull ambient lights", Color.Yellow); return; } @@ -803,11 +807,11 @@ namespace Barotrauma if (add) { - NewMessage($"Set ambient light color to {color}.", Color.White); + NewMessage($"Set ambient light color to {color}.", Color.Yellow); } else { - NewMessage($"Increased ambient light by {color}.", Color.White); + NewMessage($"Increased ambient light by {color}.", Color.Yellow); } }); AssignRelayToServer("ambientlight", false); @@ -1124,7 +1128,18 @@ namespace Barotrauma state = !GameMain.DebugDraw; } GameMain.DebugDraw = state; - NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); + }); + AssignRelayToServer("debugdraw", false); + + AssignOnExecute("debugdrawlos", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.LightManager.DebugLos; + } + GameMain.LightManager.DebugLos = state; + NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1146,7 +1161,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White); + NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("devmode", false); @@ -1157,7 +1172,7 @@ namespace Barotrauma state = !TextManager.DebugDraw; } TextManager.DebugDraw = state; - NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1170,19 +1185,19 @@ namespace Barotrauma var config = GameSettings.CurrentConfig; config.Audio.DisableVoiceChatFilters = state; GameSettings.SetCurrentConfig(config); - NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.Yellow); }); AssignRelayToServer("togglevoicechatfilters", false); commands.Add(new Command("fpscounter", "fpscounter: Toggle the FPS counter.", (string[] args) => { GameMain.ShowFPS = !GameMain.ShowFPS; - NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("showperf", "showperf: Toggle performance statistics on/off.", (string[] args) => { GameMain.ShowPerf = !GameMain.ShowPerf; - NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.White); + NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.Yellow); })); AssignOnClientExecute("netstats", (string[] args) => @@ -1194,55 +1209,55 @@ namespace Barotrauma commands.Add(new Command("hudlayoutdebugdraw|debugdrawhudlayout", "hudlayoutdebugdraw: Toggle the debug drawing mode of HUD layout areas on/off.", (string[] args) => { HUDLayoutSettings.DebugDraw = !HUDLayoutSettings.DebugDraw; - NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("interactdebugdraw|debugdrawinteract", "interactdebugdraw: Toggle the debug drawing mode of item interaction ranges on/off.", (string[] args) => { Character.DebugDrawInteract = !Character.DebugDrawInteract; - NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.White); + NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.Yellow); }, isCheat: true)); AssignOnExecute("togglehud|hud", (string[] args) => { GUI.DisableHUD = !GUI.DisableHUD; GameMain.Instance.IsMouseVisible = !GameMain.Instance.IsMouseVisible; - NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.White); + NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.Yellow); }); AssignRelayToServer("togglehud|hud", false); AssignOnExecute("toggleupperhud", (string[] args) => { GUI.DisableUpperHUD = !GUI.DisableUpperHUD; - NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.White); + NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.Yellow); }); AssignRelayToServer("toggleupperhud", false); AssignOnExecute("toggleitemhighlights", (string[] args) => { GUI.DisableItemHighlights = !GUI.DisableItemHighlights; - NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.White); + NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.Yellow); }); AssignRelayToServer("toggleitemhighlights", false); AssignOnExecute("togglecharacternames", (string[] args) => { GUI.DisableCharacterNames = !GUI.DisableCharacterNames; - NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.White); + NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.Yellow); }); AssignRelayToServer("togglecharacternames", false); AssignOnExecute("followsub", (string[] args) => { Camera.FollowSub = !Camera.FollowSub; - NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.White); + NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.Yellow); }); AssignRelayToServer("followsub", false); AssignOnExecute("toggleaitargets|aitargets", (string[] args) => { AITarget.ShowAITargets = !AITarget.ShowAITargets; - NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.White); + NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.Yellow); }); AssignRelayToServer("toggleaitargets|aitargets", false); @@ -1264,10 +1279,36 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.White); + NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); }); AssignRelayToServer("debugai", false); + AssignOnExecute("showmonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = true; + CreatureMetrics.Save(); + NewMessage("All monsters are now visible in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("showmonsters", false); + + AssignOnExecute("hidemonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = false; + CreatureMetrics.Save(); + NewMessage("All monsters that haven't yet been encountered in the game are now hidden in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("hidemonsters", false); + AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); @@ -2833,7 +2874,7 @@ namespace Barotrauma NewMessage("Valid ranks are:", Color.White); foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - NewMessage(" - " + permissionPreset.Name, Color.White); + NewMessage(" - " + permissionPreset.DisplayName, Color.White); } ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) => { @@ -2991,7 +3032,7 @@ namespace Barotrauma ThrowError($"Could not find the location type \"{args[0]}\"."); return; } - GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(locationType); + GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(GameMain.GameSession.Campaign, locationType); }, () => { @@ -3319,6 +3360,11 @@ namespace Barotrauma else { NewMessage("Level seed: " + Level.Loaded.Seed); + NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); + NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); + NewMessage("Mirrored: " + Level.Loaded.Mirrored); + NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y); + NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 18e93bfbd..0d7bb2e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -83,6 +83,7 @@ namespace Barotrauma GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), selectedButton: null); // gray out the last text block if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) { @@ -269,14 +270,7 @@ namespace Barotrauma if (actionInstance != null) { actionInstance.selectedOption = selectedOption; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); btn.ExternalHighlight = true; return true; } @@ -286,14 +280,7 @@ namespace Barotrauma SendResponse(actionId.Value, selectedOption); btn.CanBeFocused = false; btn.ExternalHighlight = true; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); return true; } //should not happen @@ -305,6 +292,18 @@ namespace Barotrauma } } + public static void SelectOption(ushort actionId, int option) + { + if (lastMessageBox.UserData is Pair userData) + { + if (userData.Second != actionId) { return; } + + GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; + Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), (btn) => btn.UserData is int i && i == option); + } + } + private static Tuple GetSizes(DialogTypes dialogTypes) { return dialogTypes switch @@ -383,6 +382,30 @@ namespace Barotrauma return buttons; } + private static void DisableButtons(IEnumerable buttons, GUIButton selectedButton) + { + DisableButtons(buttons, (btn) => btn == selectedButton); + } + + private static void DisableButtons(IEnumerable buttons, Func isSelectedButton) + { + foreach (GUIButton btn in buttons) + { + if (btn.CanBeFocused) + { + btn.CanBeFocused = false; + if (isSelectedButton(btn)) + { + btn.Selected = true; + } + else + { + btn.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + } + } + private static void SendResponse(UInt16 actionId, int selectedOption) { IWriteMessage outmsg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 9093450ec..ef02adad7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -608,58 +608,90 @@ namespace Barotrauma } break; case NetworkEventType.CONVERSATION: - UInt16 identifier = msg.ReadUInt16(); - string eventSprite = msg.ReadString(); - byte dialogType = msg.ReadByte(); - bool continueConversation = msg.ReadBoolean(); - UInt16 speakerId = msg.ReadUInt16(); - string text = msg.ReadString(); - bool fadeToBlack = msg.ReadBoolean(); - byte optionCount = msg.ReadByte(); - List options = new List(); - for (int i = 0; i < optionCount; i++) { - options.Add(msg.ReadString()); - } - - byte endCount = msg.ReadByte(); - int[] endings = new int[endCount]; - for (int i = 0; i < endCount; i++) - { - endings[i] = msg.ReadByte(); - } - - if (string.IsNullOrEmpty(text) && optionCount == 0) - { - GUIMessageBox.MessageBoxes.ForEachMod(mb => + UInt16 identifier = msg.ReadUInt16(); + string eventSprite = msg.ReadString(); + byte dialogType = msg.ReadByte(); + bool continueConversation = msg.ReadBoolean(); + UInt16 speakerId = msg.ReadUInt16(); + string text = msg.ReadString(); + bool fadeToBlack = msg.ReadBoolean(); + byte optionCount = msg.ReadByte(); + List options = new List(); + for (int i = 0; i < optionCount; i++) { - if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + options.Add(msg.ReadString()); + } + + byte endCount = msg.ReadByte(); + int[] endings = new int[endCount]; + for (int i = 0; i < endCount; i++) + { + endings[i] = msg.ReadByte(); + } + + if (string.IsNullOrEmpty(text) && optionCount == 0) + { + GUIMessageBox.MessageBoxes.ForEachMod(mb => { - (mb as GUIMessageBox)?.Close(); - } - }); + if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + { + (mb as GUIMessageBox)?.Close(); + } + }); + } + else + { + ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + } + if (Entity.FindEntityByID(speakerId) is Character speaker) + { + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.SetCustomInteract(null, null); + } + break; } - else + case NetworkEventType.CONVERSATION_SELECTED_OPTION: { - ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + UInt16 identifier = msg.ReadUInt16(); + int selectedOption = msg.ReadByte() - 1; + ConversationAction.SelectOption(identifier, selectedOption); + break; } - if (Entity.FindEntityByID(speakerId) is Character speaker) - { - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.SetCustomInteract(null, null); - } - break; case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); + int locationIndex = msg.ReadInt32(); + int destinationIndex = msg.ReadInt32(); + string missionName = msg.ReadString(); MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; + if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) + { + Location location = map.Locations[locationIndex]; + map.Discover(location, checkTalents: false); + + LocationConnection? connection = null; + if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) + { + Location destination = map.Locations[destinationIndex]; + connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); + } + if (connection != null) + { + location.UnlockMission(prefab, connection); + } + else + { + location.UnlockMission(prefab); + } + } } break; case NetworkEventType.UNLOCKPATH: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index fcd12b072..fa15748e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -8,7 +8,7 @@ namespace Barotrauma public override int State { get { return base.State; } - protected set + set { if (state != value) { @@ -45,7 +45,10 @@ namespace Barotrauma { requireRescue.Add(character); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); + } #endif } ushort itemCount = msg.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 08356c60c..1a9941f42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -10,8 +10,7 @@ namespace Barotrauma public override RichString GetMissionRewardText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - + LocalizedString rewardText = GetRewardAmountText(sub); LocalizedString retVal; if (rewardPerCrate.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..5becd9ba2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs @@ -0,0 +1,138 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + public override bool DisplayAsCompleted => false; + + public override bool DisplayAsFailed => false; + + partial void OnStateChangedProjSpecific() + { + SoundPlayer.ForceMusicUpdate(); + if (Phase == MissionPhase.NoItemsDestroyed) + { + CoroutineManager.Invoke(() => + { + if (boss != null && !boss.Removed) + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 8, fadeOut: false, startZoom: 1.0f, endZoom: 0.3f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = 3.0f + }; + } + }, delay: 3.0f); + } + else if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.StartCoroutine(wakeUpCoroutine(), name: "EndMission.wakeUpCoroutine"); + } + else if (Phase == MissionPhase.BossKilled) + { + if (!string.IsNullOrEmpty(endCinematicSound)) + { + SoundPlayer.PlaySound(endCinematicSound); + } + CoroutineManager.Invoke(() => + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = float.PositiveInfinity + }; + }, delay: 3.0f); + } + + IEnumerable wakeUpCoroutine() + { + yield return new WaitForSeconds(wakeUpCinematicDelay); + if (boss != null && !boss.Removed) + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 5.0f, fadeOut: false, losFadeIn: false, startZoom: 1.0f, endZoom: 0.4f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = cameraWaitDuration + }; + } + yield return new WaitForSeconds(bossWakeUpDelay); + if (boss != null && !boss.Removed) + { + foreach (var limb in boss.AnimController.Limbs) + { + if (!limb.FreezeBlinkState) { continue; } + limb.FreezeBlinkState = false; + if (limb.LightSource is Lights.LightSource light) + { + light.Enabled = true; + } + } + } + } + } + + partial void UpdateProjSpecific() + { + if (boss == null || boss.Removed) { return; } + if (Phase is MissionPhase.Initial or MissionPhase.NoItemsDestroyed or MissionPhase.SomeItemsDestroyed) + { + // Put asleep. + // Have to set the light every frame (or at least periodically), because light.Enabled is changed when Character.IsVisible changes (off/on screen). See GameScreen.Draw(). + foreach (var limb in boss.AnimController.Limbs) + { + if (limb.Params.BlinkFrequency > 0) + { + limb.FreezeBlinkState = true; + limb.BlinkPhase = -limb.Params.BlinkHoldTime; + if (limb.LightSource is Lights.LightSource light) + { + light.Enabled = false; + } + } + } + } + +#if DEBUG + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.O)) + { + State = 0; + } + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Y)) + { + destructibleItems.ForEach(it => it.Condition = 0.0f); + } + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.U)) + { + boss?.SetAllDamage(20000.0f, 0.0f, 0.0f); + } +#endif + } + + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + + boss = Character.ReadSpawnData(msg); + + byte minionCount = msg.ReadByte(); + List minionList = new List(); + for (int i = 0; i < minionCount; i++) + { + var minion = Character.ReadSpawnData(msg); + if (minion == null) + { + throw new System.Exception($"Error in EndMission.ClientReadInitial: failed to create a minion (mission: {Prefab.Identifier}, index: {i})"); + } + minionList.Add(minion); + } + minions = minionList.ToImmutableArray(); + if (minions.Length != minionCount) + { + throw new System.Exception("Error in EndMission.ClientReadInitial: minion count does not match the server count (" + minionCount + " != " + minions.Length + "mission: " + Prefab.Identifier + ")"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs index 6036c0586..a7a45837c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs @@ -2,7 +2,7 @@ { partial class GoToMission : Mission { - public override bool DisplayAsCompleted => false; + public override bool DisplayAsCompleted => State >= Prefab.MaxProgressState; public override bool DisplayAsFailed => false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index bb573b27d..41a7758b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } - for (int i = 0; i < resourceClusters.Count; i++) + for (int i = 0; i < resourceAmounts.Count; i++) { var amount = msg.ReadByte(); var rotation = msg.ReadSingle(); @@ -54,7 +54,7 @@ namespace Barotrauma CalculateMissionClusterPositions(); - for(int i = 0; i < resourceClusters.Count; i++) + for(int i = 0; i < resourceAmounts.Count; i++) { var identifier = msg.ReadIdentifier(); var count = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 28eafbc6c..4a0e51172 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -32,44 +32,73 @@ namespace Barotrauma return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } - public virtual RichString GetMissionRewardText(Submarine sub) + /// + /// Returns the amount of marks you get from the reward (e.g. "3,000 mk") + /// + protected LocalizedString GetRewardAmountText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); + int baseReward = GetReward(sub); + int finalReward = GetFinalReward(sub); + string rewardAmountText = string.Format(CultureInfo.InvariantCulture, "{0:N0}", baseReward); + if (finalReward > baseReward) + { + rewardAmountText += $" + {string.Format(CultureInfo.InvariantCulture, "{0:N0}", finalReward - baseReward)}"; + } + return TextManager.GetWithVariable("currencyformat", "[credits]", rewardAmountText); } - public RichString GetReputationRewardText(Location currLocation) + /// + /// Returns the full reward text of the mission (e.g. "Reward: 2,000 mk" or "Reward: 500 mk x 2 (out of max 5) = 1,000 mk") + /// + public virtual RichString GetMissionRewardText(Submarine sub) + { + LocalizedString rewardText = GetRewardAmountText(sub); + return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖" + rewardText + "‖end‖")); + } + + public RichString GetReputationRewardText() { List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - LocalizedString name = ""; - - if (reputationReward.Key == "location") + FactionPrefab targetFactionPrefab; + if (reputationReward.Key == "location" ) { - name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; + targetFactionPrefab = OriginLocation.Faction?.Prefab; } else { - var faction = FactionPrefab.Prefabs.Find(f => f.Identifier == reputationReward.Key); - if (faction != null) - { - name = $"‖color:{XMLExtensions.ColorToString(faction.IconColor)}‖{faction.Name}‖end‖"; - } - else - { - name = TextManager.Get(reputationReward.Key); - } + FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFactionPrefab); + } + + if (targetFactionPrefab == null) + { + return string.Empty; } - float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); - string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers + + float totalReputationChange = reputationReward.Value; + if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == targetFactionPrefab) is Faction faction) + { + totalReputationChange = reputationReward.Value * faction.Reputation.GetReputationChangeMultiplier(reputationReward.Value); + } + + LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(targetFactionPrefab.IconColor)}‖{targetFactionPrefab.Name}‖end‖"; + float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, totalReputationChange); + string formattedValue = ((int)Math.Round(totalReputationChange)).ToString("+#;-#;0"); //force plus sign for positive numbers LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", ("[reputationname]", name), - ("[reputationvalue]", $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); reputationRewardTexts.Add(rewardText.Value); } - return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); + if (reputationRewardTexts.Any()) + { + return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); + } + else + { + return string.Empty; + } } partial void ShowMessageProjSpecific(int missionState) @@ -107,6 +136,11 @@ namespace Barotrauma }; } + public Identifier GetOverrideMusicType() + { + return Prefab.GetOverrideMusicType(State); + } + public virtual void ClientRead(IReadMessage msg) { State = msg.ReadInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 3ec2386cf..b360f67ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -8,6 +8,7 @@ namespace Barotrauma { foreach (Mission mission in missions) { + if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox(RichString.Rich(mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index c8172bfaa..0bddcc04f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -1,11 +1,16 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class MissionPrefab : PrefabWithUintIdentifier { + private ImmutableArray portraits = new ImmutableArray(); + + public bool HasPortraits => portraits.Length > 0; + public Sprite Icon { get; @@ -49,24 +54,57 @@ namespace Barotrauma private Sprite hudIcon; private Color? hudIconColor; + private ImmutableDictionary overrideMusicOnState; + partial void InitProjSpecific(ContentXElement element) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); + Dictionary overrideMusic = new Dictionary(); + List portraits = new List(); foreach (var subElement in element.Elements()) { - string name = subElement.Name.ToString(); - if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) + switch (subElement.Name.ToString().ToLowerInvariant()) { - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); - } - else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) - { - hudIcon = new Sprite(subElement); - hudIconColor = subElement.GetAttributeColor("color"); + case "icon": + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + break; + case "hudicon": + hudIcon = new Sprite(subElement); + hudIconColor = subElement.GetAttributeColor("color"); + break; + case "overridemusic": + overrideMusic.Add( + subElement.GetAttributeInt("state", 0), + subElement.GetAttributeIdentifier("type", Identifier.Empty)); + break; + case "portrait": + var portrait = new Sprite(subElement, lazyLoad: true); + if (portrait != null) + { + portraits.Add(portrait); + } + break; } } + this.portraits = portraits.ToImmutableArray(); + overrideMusicOnState = overrideMusic.ToImmutableDictionary(); + } + + public Identifier GetOverrideMusicType(int state) + { + if (overrideMusicOnState.TryGetValue(state, out Identifier id)) + { + return id; + } + return Identifier.Empty; + } + + public Sprite GetPortrait(int randomSeed) + { + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } partial void DisposeProjectSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs index 3033769f1..d964fadc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs @@ -13,11 +13,12 @@ namespace Barotrauma byte monsterCount = msg.ReadByte(); for (int i = 0; i < monsterCount; i++) { - monsters.Add(Character.ReadSpawnData(msg)); - } - if (monsters.Contains(null)) - { - throw new System.Exception("Error in MonsterMission.ClientReadInitial: monster list contains null (mission: " + Prefab.Identifier + ")"); + var monster = Character.ReadSpawnData(msg); + if (monster == null) + { + throw new System.Exception($"Error in MonsterMission.ClientReadInitial: failed to create a monster (mission: {Prefab.Identifier}, index: {i})"); + } + monsters.Add(monster); } if (monsters.Count != monsterCount) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 12c80c496..be7a49430 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -11,38 +11,59 @@ namespace Barotrauma public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); - bool usedExistingItem = msg.ReadBoolean(); - if (usedExistingItem) + + foreach (var target in targets) { - ushort id = msg.ReadUInt16(); - item = Entity.FindEntityByID(id) as Item; - if (item == null) + bool targetFound = msg.ReadBoolean(); + if (!targetFound) { continue; } + + bool usedExistingItem = msg.ReadBoolean(); + if (usedExistingItem) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + ushort id = msg.ReadUInt16(); + target.Item = Entity.FindEntityByID(id) as Item; + if (target.Item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + } + } + else + { + target.Item = Item.ReadSpawnData(msg); + if (target.Item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + } + } + + int executedEffectCount = msg.ReadByte(); + for (int i = 0; i < executedEffectCount; i++) + { + int listIndex = msg.ReadByte(); + int effectIndex = msg.ReadByte(); + var selectedEffect = target.StatusEffects[listIndex][effectIndex]; + target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); + } + + if (target.Item.body != null) + { + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; } } - else + } + + public override void ClientRead(IReadMessage msg) + { + base.ClientRead(msg); + int targetCount = msg.ReadByte(); + for (int i = 0; i < targetCount; i++) { - item = Item.ReadSpawnData(msg); - if (item == null) + var state = (Target.RetrievalState)msg.ReadByte(); + if (i < targets.Count) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + targets[i].State = state; } } - - int executedEffectCount = msg.ReadByte(); - for (int i = 0; i < executedEffectCount; i++) - { - int index1 = msg.ReadByte(); - int index2 = msg.ReadByte(); - var selectedEffect = statusEffects[index1][index2]; - item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); - } - - if (item.body != null) - { - item.body.FarseerBody.BodyType = BodyType.Kinematic; - } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 9a33f0ce9..16ea7caa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -199,8 +199,8 @@ namespace Barotrauma if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { size = new Point( - subElement.GetAttributeInt("width", 0), - subElement.GetAttributeInt("height", 0)); + ParseSize(subElement, "width"), + ParseSize(subElement, "height")); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 85afac3c9..5af8c6e02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -3,7 +3,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; @@ -29,6 +28,8 @@ namespace Barotrauma private Point resolutionWhenCreated; + private bool needsHireableRefresh; + private enum SortingMethod { AlphabeticalAsc, @@ -50,6 +51,8 @@ namespace Barotrauma campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting( "CrewManagement.UpdateLocationView".ToIdentifier(), (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation)); + Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting( + "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true); } public void RefreshPermissions() @@ -68,7 +71,13 @@ namespace Barotrauma { if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) { - buyButton.Enabled = HasPermission; + CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; + bool enoughReputationToHire = EnoughReputationToHire(characterInfo); + buyButton.Enabled = HasPermission && enoughReputationToHire; + foreach (GUITextBlock text in child.GetAllChildren()) + { + text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); + } } } } @@ -294,18 +303,21 @@ namespace Barotrauma if (sortingMethod == SortingMethod.AlphabeticalAsc) { list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name)); } else if (sortingMethod == SortingMethod.JobAsc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - String.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? + string.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary)); if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } @@ -313,9 +325,26 @@ namespace Barotrauma { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel)); if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } } + + int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2) + { + CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo; + CharacterInfo info2 = ((InfoSkill)c2.UserData).CharacterInfo; + float requirement1 = EnoughReputationToHire(info1) ? 0 : info1.MinReputationToHire.reputation; + float requirement2 = EnoughReputationToHire(info2) ? 0 : info2.MinReputationToHire.reputation; + if (MathUtils.NearlyEqual(requirement1, 0.0f) && MathUtils.NearlyEqual(requirement2, 0.0f)) + { + return null; + } + else + { + return requirement1.CompareTo(requirement2); + } + } } private readonly struct InfoSkill @@ -367,12 +396,25 @@ namespace Barotrauma nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), - characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) + characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) { CanBeFocused = false }; - jobBlock.Text = ToolBox.LimitString(jobBlock.Text, jobBlock.Font, jobBlock.Rect.Width); - + if (!characterInfo.MinReputationToHire.factionId.IsEmpty) + { + var faction = campaign.Factions.Find(f => f.Prefab.Identifier == characterInfo.MinReputationToHire.factionId); + if (faction != null) + { + jobBlock.TextColor = faction.Prefab.IconColor; + } + } + var fullJobText = jobBlock.Text; + jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width); + if (jobBlock.Text != fullJobText) + { + jobBlock.ToolTip = fullJobText; + jobBlock.CanBeFocused = true; + } float width = 0.6f / 3; if (characterInfo.Job != null && skill != null) { @@ -410,7 +452,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = HasPermission, + Enabled = CanHire(characterInfo), OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => @@ -426,10 +468,9 @@ namespace Barotrauma else if (!btn.Enabled) { btn.ToolTip = string.Empty; - btn.Enabled = HasPermission; + btn.Enabled = CanHire(characterInfo); } }; - } else if (listBox == pendingList) { @@ -437,7 +478,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = HasPermission, + Enabled = CanHire(characterInfo), OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) }; } @@ -474,12 +515,30 @@ namespace Barotrauma size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) { - Enabled = HasPermission, - ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", TextManager.Get($"input.{(PlayerInput.MouseButtonsSwapped() ? "rightmouse" : "leftmouse")}")), + Enabled = CanHire(characterInfo), + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), UserData = characterInfo, OnClicked = CreateRenamingComponent }; } + + bool CanHire(CharacterInfo characterInfo) + { + if (!HasPermission) { return false; } + return EnoughReputationToHire(characterInfo); + } + } + + private bool EnoughReputationToHire(CharacterInfo characterInfo) + { + if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) + { + if (campaign.GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + { + return false; + } + } + return true; } private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -488,13 +547,13 @@ namespace Barotrauma Point absoluteOffset = new Point( pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5, characterFrame.Rect.Top); - int frameSize = (int)(GUI.Scale * 300); - if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize) < 0) + Point frameSize = new Point(GUI.IntScale(300), GUI.IntScale(350)); + if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize.Y) < 0) { pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight; absoluteOffset.Y = characterFrame.Rect.Bottom; } - characterPreviewFrame = new GUIFrame(new RectTransform(new Point(frameSize), parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) + characterPreviewFrame = new GUIFrame(new RectTransform(frameSize, parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) { AbsoluteOffset = absoluteOffset }, style: "InnerFrame") @@ -503,7 +562,8 @@ namespace Barotrauma }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center)) { - RelativeSpacing = 0.01f + RelativeSpacing = 0.01f, + Stretch = true }; // Character info @@ -545,9 +605,23 @@ namespace Barotrauma blockHeight = 1.0f / characterSkills.Count(); foreach (Skill skill in characterSkills) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier), font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right); } + + if (characterInfo.MinReputationToHire.reputation > 0.0f) + { + var repStr = TextManager.GetWithVariables( + "campaignstore.reputationrequired", + ("[amount]", ((int)characterInfo.MinReputationToHire.reputation).ToString()), + ("[faction]", TextManager.Get("faction." + characterInfo.MinReputationToHire.factionId).Value)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), mainGroup.RectTransform), + repStr, textColor: !EnoughReputationToHire(characterInfo) ? GUIStyle.Orange : GUIStyle.Green, + font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center); + } + mainGroup.Recalculate(); + characterPreviewFrame.RectTransform.MinSize = + new Point(0, (int)(mainGroup.Children.Sum(c => c.Rect.Height + mainGroup.Rect.Height * mainGroup.RelativeSpacing) / mainGroup.RectTransform.RelativeSize.Y)); } private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -636,7 +710,7 @@ namespace Barotrauma List nonDuplicateHires = new List(); hires.ForEach(hireInfo => { - if(campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) + if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) { nonDuplicateHires.Add(hireInfo); } @@ -791,6 +865,16 @@ namespace Barotrauma playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + if (needsHireableRefresh) + { + RefreshCrewFrames(hireableList); + if (sortingDropDown?.SelectedItemData != null) + { + SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); + } + needsHireableRefresh = false; + } + (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn); if (highlightedFrame != null && highlightedInfo != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 9a84a1b59..838c2ee27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -106,6 +106,11 @@ namespace Barotrauma public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X); + /// + /// A horizontal scaling factor for low aspect ratios (small width relative to height) + /// + public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; public static int UIWidth @@ -140,13 +145,20 @@ namespace Barotrauma public static Texture2D WhiteTexture => solidWhiteTexture; private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite; - private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; - private static int debugDrawMetadataOffset; - private static readonly string[] ignoredMetadataInfo = { string.Empty, string.Empty, string.Empty, string.Empty }; + private static bool debugDrawSounds, debugDrawEvents; + + private static DebugDrawMetaData debugDrawMetaData; + + public struct DebugDrawMetaData + { + public bool Enabled; + public bool FactionMetadata, UpgradeLevels, UpgradePrices; + public int Offset; + } public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice; - private static List messages = new List(); + private static readonly List messages = new List(); public static GUIFrame PauseMenu { get; private set; } public static GUIFrame SettingsMenuContainer { get; private set; } @@ -195,8 +207,9 @@ namespace Barotrauma SettingsMenuOpen || DebugConsole.IsOpen || GameSession.IsTabMenuOpen || - (GameMain.GameSession?.GameMode?.Paused ?? false) || - CharacterHUD.IsCampaignInterfaceOpen; + GameMain.GameSession?.GameMode is { Paused: true } || + CharacterHUD.IsCampaignInterfaceOpen || + GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } }; } } @@ -533,18 +546,17 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { // TODO: TEST THIS - if (debugDrawMetadata) + if (debugDrawMetaData.Enabled) { string text = "Ctrl+M to hide campaign metadata debug info\n\n" + - $"Ctrl+1 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[0]) ? "hide" : "show")} outpost reputations, \n" + - $"Ctrl+2 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[1]) ? "hide" : "show")} faction reputations, \n" + - $"Ctrl+3 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[2]) ? "hide" : "show")} upgrade levels, \n" + - $"Ctrl+4 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[3]) ? "hide" : "show")} upgrade prices"; + $"Ctrl+1 to {(debugDrawMetaData.FactionMetadata ? "hide" : "show")} faction reputations, \n" + + $"Ctrl+2 to {(debugDrawMetaData.UpgradeLevels ? "hide" : "show")} upgrade levels, \n" + + $"Ctrl+3 to {(debugDrawMetaData.UpgradePrices ? "hide" : "show")} upgrade prices"; Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = new Vector2(GameMain.GraphicsWidth - (textSize.X + 10), 300); DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); pos.Y += textSize.Y + 8; - campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, debugDrawMetadataOffset, ignoredMetadataInfo); + campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, campaignMode, debugDrawMetaData); } else { @@ -684,37 +696,24 @@ namespace Barotrauma } } - public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, float aberrationStrength = 1.0f) + public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color, Rectangle? drawArea = null, SpriteEffects spriteEffects = SpriteEffects.None) { - double aberrationT = (Timing.TotalTime * 0.5f); - GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f * aberrationStrength); - GameMain.GameScreen.PostProcessEffect.Parameters["chromaticAberrationStrength"].SetValue(new Vector3(-0.025f, -0.01f, -0.05f) * - (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f) * aberrationStrength); - - Matrix.CreateOrthographicOffCenter(0, GameMain.GraphicsWidth, GameMain.GraphicsHeight, 0, 0, -1, out Matrix projection); - - GameMain.GameScreen.PostProcessEffect.Parameters["MatrixTransform"].SetValue(projection); - GameMain.GameScreen.PostProcessEffect.CurrentTechnique = GameMain.GameScreen.PostProcessEffect.Techniques["BlurChromaticAberration"]; - GameMain.GameScreen.PostProcessEffect.CurrentTechnique.Passes[0].Apply(); - - spriteBatch.Begin(SpriteSortMode.Immediate, effect: GameMain.GameScreen.PostProcessEffect); + Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); float scale = Math.Max( - (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, - (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; - float paddingX = backgroundSprite.SourceRect.Width * scale - GameMain.GraphicsWidth; - float paddingY = backgroundSprite.SourceRect.Height * scale - GameMain.GraphicsHeight; + (float)area.Width / backgroundSprite.SourceRect.Width, + (float)area.Height / backgroundSprite.SourceRect.Height) * 1.1f; + float paddingX = backgroundSprite.SourceRect.Width * scale - area.Width; + float paddingY = backgroundSprite.SourceRect.Height * scale - area.Height; - double noiseT = (Timing.TotalTime * 0.02f); + double noiseT = Timing.TotalTime * 0.02f; Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); spriteBatch.Draw(backgroundSprite.Texture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, backgroundSprite.size / 2, - scale, SpriteEffects.None, 0.0f); - - spriteBatch.End(); + area.Center.ToVector2() + pos, + null, color, 0.0f, backgroundSprite.size / 2, + scale, spriteEffects, 0.0f); } #region Update list @@ -1206,48 +1205,37 @@ namespace Barotrauma } if (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.M)) { - debugDrawMetadata = !debugDrawMetadata; + debugDrawMetaData.Enabled = !debugDrawMetaData.Enabled; } - if (debugDrawMetadata) + if (debugDrawMetaData.Enabled) { if (PlayerInput.KeyHit(Keys.Up)) { - debugDrawMetadataOffset--; + debugDrawMetaData.Offset--; } - if (PlayerInput.KeyHit(Keys.Down)) { - debugDrawMetadataOffset++; + debugDrawMetaData.Offset++; } - if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.D1)) { - ignoredMetadataInfo[0] = ignoredMetadataInfo[0] == string.Empty ? "reputation.location" : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.FactionMetadata = !debugDrawMetaData.FactionMetadata; + debugDrawMetaData.Offset = 0; } - if (PlayerInput.KeyHit(Keys.D2)) { - ignoredMetadataInfo[1] = ignoredMetadataInfo[1] == string.Empty ? "reputation.faction" : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.UpgradeLevels = !debugDrawMetaData.UpgradeLevels; + debugDrawMetaData.Offset = 0; } - if (PlayerInput.KeyHit(Keys.D3)) { - ignoredMetadataInfo[2] = ignoredMetadataInfo[2] == string.Empty ? "upgrade." : string.Empty; - debugDrawMetadataOffset = 0; - } - - if (PlayerInput.KeyHit(Keys.D4)) - { - ignoredMetadataInfo[3] = ignoredMetadataInfo[3] == string.Empty ? "upgradeprice." : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.UpgradePrices = !debugDrawMetaData.UpgradePrices; + debugDrawMetaData.Offset = 0; } } - } HandlePersistingElements(deltaTime); @@ -2599,8 +2587,12 @@ namespace Barotrauma public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) { - if (messages.Any(msg => msg.Text == message)) { return; } - messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont)); + var guiMessage = new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont); + lock (mutex) + { + if (messages.Any(msg => msg.Text == message)) { return; } + messages.Add(guiMessage); + } if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } @@ -2610,34 +2602,37 @@ namespace Barotrauma var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub); if (playSound) { SoundPlayer.PlayUISound(soundType); } - bool overlapFound = true; - int tries = 0; - while (overlapFound) - { - overlapFound = false; - foreach (var otherMessage in messages) - { - float xDiff = otherMessage.Pos.X - newMessage.Pos.X; - if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } - float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; - if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } - Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); - if (moveDir.LengthSquared() > 0.0001f) - { - moveDir = Vector2.Normalize(moveDir); - } - else - { - moveDir = Rand.Vector(1.0f); - } - moveDir.Y = -Math.Abs(moveDir.Y); - newMessage.Pos -= Vector2.UnitY * 10; - } - tries++; - if (tries > 20) { break; } - } - messages.Add(newMessage); + lock (mutex) + { + bool overlapFound = true; + int tries = 0; + while (overlapFound) + { + overlapFound = false; + foreach (var otherMessage in messages) + { + float xDiff = otherMessage.Pos.X - newMessage.Pos.X; + if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } + float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; + if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } + Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); + if (moveDir.LengthSquared() > 0.0001f) + { + moveDir = Vector2.Normalize(moveDir); + } + else + { + moveDir = Rand.Vector(1.0f); + } + moveDir.Y = -Math.Abs(moveDir.Y); + newMessage.Pos -= Vector2.UnitY * 10; + } + tries++; + if (tries > 20) { break; } + } + messages.Add(newMessage); + } } public static void ClearMessages() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index d27e2c086..10334efc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,9 +8,7 @@ namespace Barotrauma { public class GUICanvas : RectTransform { - private static readonly object mutex = new object(); - - protected GUICanvas() : base(size, parent: null) { } + protected GUICanvas() : base(Size, parent: null) { } private static GUICanvas _instance; public static GUICanvas Instance @@ -33,7 +31,7 @@ namespace Barotrauma //GUICanvas stores the children as weak references, to allow elements that we no longer need to get garbage collected private readonly List> childrenWeakRef = new List>(); - private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); + private static Vector2 Size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); protected override Rectangle NonScaledUIRect => UIRect; @@ -41,25 +39,27 @@ namespace Barotrauma private static void OnChildrenChanged(RectTransform _) { - lock (mutex) + CrossThread.RequestExecutionOnMainThread(RefreshChildren); + } + + private static void RefreshChildren() + { + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) { - //add weak reference if we don't have one yet - foreach (var child in _instance.Children) + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) { - if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) - { - _instance.childrenWeakRef.Add(new WeakReference(child)); - } + _instance.childrenWeakRef.Add(new WeakReference(child)); } - //get rid of strong references - _instance.children.Clear(); - //remove dead children - for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) + } + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 1; i >= 0; i--) + { + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) { - if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) - { - _instance.childrenWeakRef.RemoveAt(i); - } + _instance.childrenWeakRef.RemoveAt(i); } } } @@ -67,7 +67,7 @@ namespace Barotrauma // Turn public, if there is a need to call this manually. private static void RecalculateSize() { - Vector2 recalculatedSize = size; + Vector2 recalculatedSize = Size; // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well for (int i = 0; i < Instance.childrenWeakRef.Count; i++) @@ -109,7 +109,7 @@ namespace Barotrauma } } - Instance.Resize(size, resizeChildren: true); + Instance.Resize(Size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); _instance.children.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index dd848bd65..8a795ccd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -244,18 +244,16 @@ namespace Barotrauma return parentHierarchy.Last(); } - public void AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null) + public GUIComponent AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null, Color? color = null, Color? textColor = null) { toolTip ??= ""; if (selectMultiple) { - var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, style: "ListBoxElement") + var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color) { UserData = userData, ToolTip = toolTip }; - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text) { UserData = userData, @@ -275,7 +273,7 @@ namespace Barotrauma foreach (GUIComponent child in ListBox.Content.Children) { var tickBox = child.GetChild(); - if (tickBox.Selected) + if (tickBox is { Selected: true }) { selectedDataMultiple.Add(child.UserData); selectedIndexMultiple.Add(i); @@ -289,11 +287,11 @@ namespace Barotrauma return true; } }; + return frame; } else { - new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, text, style: "ListBoxElement") + return new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor) { UserData = userData, ToolTip = toolTip @@ -323,7 +321,7 @@ namespace Barotrauma } else { - if (!(component is GUITextBlock textBlock)) + if (component is not GUITextBlock textBlock) { textBlock = component.GetChild(); if (textBlock is null && !AllowNonText) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index b03bc27a6..aa1a6a882 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -1059,6 +1059,7 @@ namespace Barotrauma GUIComponent child = Content.GetChild(childIndex); if (child is null) { return; } + if (!child.Enabled) { return; } bool wasSelected = true; if (OnSelected != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index fc1ade91e..4d3f69ea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -268,7 +268,7 @@ namespace Barotrauma Buttons.Clear(); } - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true, textColor: GUIStyle.TextColorBright); GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 4f938a89f..9edfae736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -313,7 +313,9 @@ namespace Barotrauma break; } - RectTransform.MinSize = TextBox.RectTransform.MinSize; + RectTransform.MinSize = new Point( + Math.Max(rectT.MinSize.X, TextBox.RectTransform.MinSize.X), + Math.Max(rectT.MinSize.Y, TextBox.RectTransform.MinSize.Y)); LayoutGroup.Recalculate(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index c9a7631cb..ee15c0c06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -1,14 +1,12 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; -using System.Text; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -20,6 +18,26 @@ namespace Barotrauma { return element.NameAsIdentifier(); } + + protected int ParseSize(XElement element, string attributeName) + { + string valueStr = element.GetAttributeString(attributeName, string.Empty); + bool relativeToWidth = valueStr.EndsWith("vw"); + bool relativeToHeight = valueStr.EndsWith("vh"); + if (relativeToWidth || relativeToHeight) + { + string floatStr = valueStr.Substring(0, valueStr.Length - 2); + if (!float.TryParse(floatStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float relativeHeight)) + { + DebugConsole.ThrowError($"Error while parsing a {nameof(GUIComponentStyle)}: {valueStr} is not a valid size."); + } + return (int)(relativeHeight / 100.0f * (relativeToWidth ? GameMain.GraphicsWidth : GameMain.GraphicsHeight)); + } + else + { + return element.GetAttributeInt(attributeName, 0); + } + } } public abstract class GUISelector where T : GUIPrefab @@ -166,7 +184,8 @@ namespace Barotrauma Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { - return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.CurrentConfig.Graphics.TextScale); + int rawSize = ParseSize(subElement, "size"); + return (uint)Math.Round(rawSize * GameSettings.CurrentConfig.Graphics.TextScale); } } return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 4972e95d3..1ae121338 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -438,7 +438,7 @@ namespace Barotrauma } else { - if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) + if ((PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked()) && selected) { if (!mouseHeldInside) { Deselect(); } mouseHeldInside = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index dec3485bb..8442731b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -143,14 +143,13 @@ namespace Barotrauma } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); - HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); int messageAreaWidth = GameMain.GraphicsWidth / 3; MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom + ButtonAreaTop.Height, messageAreaWidth, ButtonAreaTop.Height); - bool isFourByThree = GUI.IsFourByThree(); - int chatBoxWidth = !isFourByThree ? (int)(475 * GUI.Scale) : (int)(375 * GUI.Scale); + int chatBoxWidth = (int)(475 * GUI.Scale * GUI.AspectRatioAdjustment); int chatBoxHeight = (int)Math.Max(GameMain.GraphicsHeight * 0.25f, 150); ChatBoxArea = new Rectangle(Padding, GameMain.GraphicsHeight - Padding - chatBoxHeight, chatBoxWidth, chatBoxHeight); @@ -160,8 +159,9 @@ namespace Barotrauma int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; - CrewArea = new Rectangle(Padding, crewAreaY, (int)Math.Max(400 * GUI.Scale, 220), crewAreaHeight); + float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f; + CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); @@ -187,19 +187,26 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch) { - DrawRectangle(ButtonAreaTop, Color.White * 0.5f); - DrawRectangle(TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); - DrawRectangle(MessageAreaTop, GUIStyle.Orange * 0.5f); - DrawRectangle(CrewArea, Color.Blue * 0.5f); - DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); - DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f); - DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); - DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); - DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); - DrawRectangle(ItemHUDArea, Color.Magenta * 0.3f); + DrawRectangle(nameof(ButtonAreaTop), ButtonAreaTop, Color.White * 0.5f); + DrawRectangle(nameof(TutorialObjectiveListArea), TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); + DrawRectangle(nameof(MessageAreaTop), MessageAreaTop, GUIStyle.Orange * 0.5f); + DrawRectangle(nameof(CrewArea), CrewArea, Color.Blue * 0.5f); + DrawRectangle(nameof(ChatBoxArea), ChatBoxArea, Color.Cyan * 0.5f); + DrawRectangle(nameof(HealthBarArea), HealthBarArea, Color.Red * 0.5f); + DrawRectangle(nameof(HealthBarAfflictionArea), HealthBarAfflictionArea, Color.Red * 0.5f); + DrawRectangle(nameof(InventoryAreaLower), InventoryAreaLower, Color.Yellow * 0.5f); + DrawRectangle(nameof(HealthWindowAreaLeft), HealthWindowAreaLeft, Color.Red * 0.5f); + DrawRectangle(nameof(BottomRightInfoArea), BottomRightInfoArea, Color.Green * 0.5f); + DrawRectangle(nameof(ItemHUDArea), ItemHUDArea, Color.Magenta * 0.3f); - void DrawRectangle(Rectangle r, Color c) => GUI.DrawRectangle(spriteBatch, r, c); + void DrawRectangle(string label, Rectangle r, Color c) + { + if (!label.IsNullOrEmpty()) + { + GUI.DrawString(spriteBatch, r.Location.ToVector2() + Vector2.One * 3, label, c, font: GUIStyle.SmallFont); + } + GUI.DrawRectangle(spriteBatch, r, c); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index b8026a67c..0c5c6cc17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -11,9 +11,9 @@ namespace Barotrauma { class LoadingScreen { - private readonly Texture2D defaultBackgroundTexture, overlay; + private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; - private Texture2D currentBackgroundTexture; + private Sprite currentBackgroundTexture; private readonly Sprite noiseSprite; private string randText = ""; @@ -24,6 +24,8 @@ namespace Barotrauma private Video currSplashScreen; private DateTime videoStartTime; + private bool mirrorBackground; + public struct PendingSplashScreen { public string Filename; @@ -112,12 +114,12 @@ namespace Barotrauma public LoadingScreen(GraphicsDevice graphics) { - defaultBackgroundTexture = TextureLoader.FromFile("Content/Map/LocationPortraits/AlienRuins.png"); + defaultBackgroundTexture = new Sprite("Content/Map/LocationPortraits/MainMenu1.png", Vector2.Zero); decorativeMap = new SpriteSheet("Content/Map/MapHUD.png", 6, 5, Vector2.Zero, sourceRect: new Rectangle(0, 0, 2048, 640)); decorativeGraph = new SpriteSheet("Content/Map/MapHUD.png", 4, 10, Vector2.Zero, sourceRect: new Rectangle(1025, 1259, 1024, 732)); - overlay = TextureLoader.FromFile("Content/UI/LoadingScreenOverlay.png"); + overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; SetSelectedTip(TextManager.Get("LoadingScreenTip")); @@ -138,35 +140,24 @@ namespace Barotrauma DisableSplashScreen(); } } - - var titleStyle = GUIStyle.GetComponentStyle("TitleText"); - Sprite titleSprite = null; - if (!WaitForLanguageSelection && titleStyle != null && titleStyle.Sprites.ContainsKey(GUIComponent.ComponentState.None)) - { - titleSprite = titleStyle.Sprites[GUIComponent.ComponentState.None].First()?.Sprite; - } drawn = true; currentBackgroundTexture ??= defaultBackgroundTexture; + float overlayScale = Math.Min(GameMain.GraphicsWidth / overlay.size.X, GameMain.GraphicsHeight / overlay.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(overlay.size.X * overlayScale / 2), 0, + (int)(GameMain.GraphicsWidth - overlay.size.X * overlayScale / 2), GameMain.GraphicsHeight); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); - float scale = (GameMain.GraphicsWidth / (float)currentBackgroundTexture.Width) * 1.2f; - float paddingX = currentBackgroundTexture.Width * scale - GameMain.GraphicsWidth; - float paddingY = currentBackgroundTexture.Height * scale - GameMain.GraphicsHeight; - - double noiseT = (Timing.TotalTime * 0.02f); - Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); - pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); - - spriteBatch.Draw(currentBackgroundTexture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, new Vector2(currentBackgroundTexture.Width / 2, currentBackgroundTexture.Height / 2), - scale, SpriteEffects.None, 0.0f); - - spriteBatch.Draw(overlay, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), null, Color.White, 0.0f, Vector2.Zero, SpriteEffects.None, 0.0f); + GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea, + spriteEffects: mirrorBackground ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); + double noiseT = Timing.TotalTime * 0.02f; float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), @@ -174,10 +165,7 @@ namespace Barotrauma color: Color.White * noiseStrength * 0.1f, textureScale: Vector2.One * noiseScale); - titleSprite?.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth * 0.05f, GameMain.GraphicsHeight * 0.125f), - Color.White, origin: new Vector2(0.0f, titleSprite.SourceRect.Height / 2.0f), - scale: GameMain.GraphicsHeight / 2000.0f); - + Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.75f)); if (WaitForLanguageSelection) { DrawLanguageSelectionPrompt(spriteBatch, graphics); @@ -215,16 +203,18 @@ namespace Barotrauma #endif } } + if (GUIStyle.LargeFont.HasValue) { GUIStyle.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), - new Vector2(GameMain.GraphicsWidth / 2.0f - GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), + textPos, Color.White); + textPos.Y += GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).Y * 1.2f; } if (GUIStyle.Font.HasValue && selectedTip != null) { - string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.5f, GUIStyle.Font.Value); + string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.3f, GUIStyle.Font.Value); string[] lines = wrappedTip.Split('\n'); float lineHeight = GUIStyle.Font.MeasureString(selectedTip).Y; @@ -234,7 +224,8 @@ namespace Barotrauma for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, + new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), + Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTip.RichTextData.Value, rtdOffset); rtdOffset += lines[i].Length; } @@ -244,7 +235,8 @@ namespace Barotrauma for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawString(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), + new Color(228, 217, 167, 255)); } } } @@ -257,13 +249,16 @@ namespace Barotrauma Vector2 decorativeScale = new Vector2(GameMain.GraphicsHeight / 1080.0f); float noiseVal = (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.25f, Timing.TotalTime * 0.5f, 0); - decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), - new Vector2(GameMain.GraphicsWidth * 0.001f, GameMain.GraphicsHeight * 0.24f), - Color.White, Vector2.Zero, 0.0f, decorativeScale, SpriteEffects.FlipVertically); + if (!WaitForLanguageSelection) + { + decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), + new Vector2(GameMain.GraphicsWidth * 0.001f, textPos.Y), + Color.White, new Vector2(0, decorativeMap.FrameSize.Y), 0.0f, decorativeScale, SpriteEffects.FlipVertically); + } decorativeMap.Draw(spriteBatch, (int)(decorativeMap.FrameCount * noiseVal), - new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.66f), - Color.White, decorativeMap.FrameSize.ToVector2(), 0.0f, decorativeScale); + new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.01f), + Color.White, new Vector2(decorativeMap.FrameSize.X, 0), 0.0f, decorativeScale, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically); if (noiseVal < 0.2f) { @@ -285,8 +280,9 @@ namespace Barotrauma if (GUIStyle.LargeFont.HasValue) { + Vector2 textSize = GUIStyle.LargeFont.MeasureString(randText); GUIStyle.LargeFont.DrawString(spriteBatch, randText, - new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), + new Vector2(GameMain.GraphicsWidth * 0.95f - textSize.X, GameMain.GraphicsHeight * 0.06f), Color.White * (1.0f - noiseVal)); } @@ -312,8 +308,8 @@ namespace Barotrauma languageSelectionCursor = new Sprite("Content/UI/cursor.png", Vector2.Zero); } - Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.3f); - Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / AvailableLanguages.Length); + Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.3f)); + Vector2 textSpacing = new Vector2(0.0f, GameMain.GraphicsHeight * 0.5f / AvailableLanguages.Length); foreach (LanguageIdentifier language in AvailableLanguages) { string localizedLanguageName = TextManager.GetTranslatedLanguageName(language); @@ -321,10 +317,10 @@ namespace Barotrauma Vector2 textSize = font.MeasureString(localizedLanguageName); bool hover = - Math.Abs(PlayerInput.MousePosition.X - textPos.X) < textSize.X / 2 && - Math.Abs(PlayerInput.MousePosition.Y - textPos.Y) < textSpacing.Y / 2; + PlayerInput.MousePosition.X > textPos.X && PlayerInput.MousePosition.X < textPos.X + textSize.X && + PlayerInput.MousePosition.Y > textPos.Y && PlayerInput.MousePosition.Y < textPos.Y + textSize.Y; - font.DrawString(spriteBatch, localizedLanguageName, textPos - textSize / 2, + font.DrawString(spriteBatch, localizedLanguageName, textPos, hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { @@ -431,7 +427,12 @@ namespace Barotrauma drawn = false; LoadState = null; SetSelectedTip(TextManager.Get("LoadingScreenTip")); - currentBackgroundTexture = LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; + currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); + if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) + { + currentBackgroundTexture = missions.Where(m => m.Prefab.HasPortraits).First().Prefab.GetPortrait(Rand.Int(int.MaxValue)); + } + mirrorBackground = Rand.Range(0.0f, 1.0f) < 0.5f; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 6f5272743..c43df5f39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -143,33 +143,32 @@ namespace Barotrauma { public readonly MedicalClinic.NetAffliction Target; public readonly ImmutableArray ElementsToDisable; + public readonly GUIComponent TargetElement; - public PopupAffliction(ImmutableArray elementsToDisable, MedicalClinic.NetAffliction target) + public PopupAffliction(ImmutableArray elementsToDisable, GUIComponent component, MedicalClinic.NetAffliction target) { Target = target; ElementsToDisable = elementsToDisable; + TargetElement = component; } } private readonly struct PopupAfflictionList { public readonly MedicalClinic.NetCrewMember Target; + public readonly GUIListBox ListElement; public readonly GUIButton TreatAllButton; - public readonly List Afflictions; + public readonly HashSet Afflictions; - public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIButton treatAllButton) + public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIListBox listElement, GUIButton treatAllButton) { + ListElement = listElement; Target = crewMember; TreatAllButton = treatAllButton; - Afflictions = new List(); + Afflictions = new HashSet(); } } - // private enum SortMode - // { - // Severity - // } - private readonly MedicalClinic medicalClinic; private readonly GUIComponent container; private Point prevResolution; @@ -221,23 +220,22 @@ namespace Barotrauma private void UpdatePopupAfflictions() { - if (selectedCrewAfflictionList is { } afflictionList) - { - foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) - { - ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); - if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) - { - ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); - } - } + if (selectedCrewAfflictionList is not { } afflictionList) { return; } - afflictionList.TreatAllButton.Enabled = true; - if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) + { + ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); + if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) { - afflictionList.TreatAllButton.Enabled = false; + ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); } } + + afflictionList.TreatAllButton.Enabled = true; + if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + { + afflictionList.TreatAllButton.Enabled = false; + } } private void UpdatePending() @@ -309,7 +307,7 @@ namespace Barotrauma } } - private void UpdateCrewPanel() + public void UpdateCrewPanel() { if (crewHealList is not { } healList) { return; } @@ -502,7 +500,7 @@ namespace Barotrauma return true; } }; - + crewHealList = new CrewHealList(crewList, parent, treatAllButton); void OnReceived(MedicalClinic.CallbackOnlyRequest obj) @@ -526,7 +524,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { - TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)(info.Character?.HealthPercentage ?? 100f)}"), + TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(info.Character?.HealthPercentage ?? 100f)}"), TextColor = GUIStyle.Green }; @@ -789,7 +787,7 @@ namespace Barotrauma GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform)) { Visible = false }; - PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, treatAllButton); + PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, afflictionList, treatAllButton); selectedCrewElement = mainFrame; selectedCrewAfflictionList = popupAfflictionList; @@ -810,9 +808,9 @@ namespace Barotrauma List allComponents = new List(); foreach (MedicalClinic.NetAffliction affliction in request.Afflictions) { - ImmutableArray createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); - allComponents.AddRange(createdComponents); - popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents, affliction)); + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); + allComponents.AddRange(createdComponents.AllCreatedElements); + popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, affliction)); } allComponents.Add(treatAllButton); @@ -832,9 +830,11 @@ namespace Barotrauma } } - private ImmutableArray CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) + private readonly record struct CreatedPopupAfflictionElement(GUIComponent MainElement, ImmutableArray AllCreatedElements); + + private CreatedPopupAfflictionElement CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) { - if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } + ToolBox.ThrowIfNull(affliction.Prefab); GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), backgroundFrame.RectTransform, Anchor.BottomCenter), style: "HorizontalLine"); @@ -846,9 +846,9 @@ namespace Barotrauma GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; - Color iconColor = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength); + Color iconColor = CharacterHealth.GetAfflictionIconColor(affliction.Prefab, affliction.Strength); - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), prefab.Icon, scaleToFit: true) + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), affliction.Prefab.Icon, scaleToFit: true) { Color = iconColor, DisabledColor = iconColor * 0.5f @@ -856,7 +856,7 @@ namespace Barotrauma GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); - GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), affliction.Prefab.Name, font: GUIStyle.SubHeadingFont); Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); @@ -878,7 +878,7 @@ namespace Barotrauma AutoScaleHorizontal = true }; - EnsureTextDoesntOverflow(prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + EnsureTextDoesntOverflow(affliction.Prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -923,7 +923,7 @@ namespace Barotrauma return true; }; - return elementsToDisable; + return new CreatedPopupAfflictionElement(backgroundFrame, elementsToDisable); } private void AddPending(ImmutableArray elementsToDisable, MedicalClinic.NetCrewMember crewMember, ImmutableArray afflictions) @@ -1033,11 +1033,53 @@ namespace Barotrauma } } + public void UpdateAfflictions(MedicalClinic.NetCrewMember crewMember) + { + if (selectedCrewAfflictionList is not { } afflictionList || !afflictionList.Target.CharacterEquals(crewMember)) { return; } + + List allComponents = new List(); + foreach (PopupAffliction existingAffliction in afflictionList.Afflictions.ToHashSet()) + { + if (crewMember.Afflictions.None(received => received.AfflictionEquals(existingAffliction.Target))) + { + // remove from UI + existingAffliction.TargetElement.RectTransform.Parent = null; + afflictionList.Afflictions.Remove(existingAffliction); + } + else + { + allComponents.AddRange(existingAffliction.ElementsToDisable); + } + } + + foreach (MedicalClinic.NetAffliction received in crewMember.Afflictions) + { + // we're not that concerned about updating the strength of the afflictions + if (afflictionList.Afflictions.Any(existing => existing.Target.AfflictionEquals(received))) { continue; } + + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.ListElement.Content, crewMember, received); + allComponents.AddRange(createdComponents.AllCreatedElements); + afflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, received)); + } + + allComponents.Add(afflictionList.TreatAllButton); + afflictionList.TreatAllButton.OnClicked = (_, _) => + { + var afflictions = crewMember.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); + if (!afflictions.Any()) { return true; } + + AddPending(allComponents.ToImmutableArray(), crewMember, afflictions); + return true; + }; + + UpdatePopupAfflictions(); + } + public void ClosePopup() { if (selectedCrewElement is { } popup) { - popup.Parent?.RemoveChild(selectedCrewElement); + popup.RectTransform.Parent = null; } selectedCrewElement = null; @@ -1096,5 +1138,14 @@ namespace Barotrauma refreshTimer = 0; } } + + public void OnDeselected() + { + if (GameMain.NetworkMember is not null) + { + MedicalClinic.SendUnsubscribeRequest(); + } + ClosePopup(); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 7eb393015..1cb5e37ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -207,11 +207,12 @@ namespace Barotrauma cargoManager.OnItemsInSellFromSubCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsSellingFromSubRefresh = true); } - public void SelectStore(Identifier identifier) + public void SelectStore(Character merchant) { + Identifier storeIdentifier = merchant?.MerchantIdentifier ?? Identifier.Empty; if (CurrentLocation?.Stores != null) { - if (!identifier.IsEmpty && CurrentLocation.GetStore(identifier) is { } store) + if (!storeIdentifier.IsEmpty && CurrentLocation.GetStore(storeIdentifier) is { } store) { ActiveStore = store; if (storeNameBlock != null) @@ -223,12 +224,13 @@ namespace Barotrauma } storeNameBlock.SetRichText(storeName); } + ActiveStore.SetMerchantFaction(merchant.Faction); } else { ActiveStore = null; string errorId, msg; - if (identifier.IsEmpty) + if (storeIdentifier.IsEmpty) { errorId = "Store.SelectStore:IdentifierEmpty"; msg = $"Error selecting store at {CurrentLocation}: identifier is empty."; @@ -236,7 +238,7 @@ namespace Barotrauma else { errorId = "Store.SelectStore:StoreDoesntExist"; - msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; } DebugConsole.LogError(msg); GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); @@ -249,17 +251,17 @@ namespace Barotrauma if (campaignUI.Campaign.Map == null) { errorId = "Store.SelectStore:MapNull"; - msg = $"Error selecting store with identifier \"{identifier}\": Map is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": Map is null."; } else if (CurrentLocation == null) { errorId = "Store.SelectStore:CurrentLocationNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation is null."; } else if (CurrentLocation.Stores == null) { errorId = "Store.SelectStore:StoresNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation.Stores is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation.Stores is null."; } if (!msg.IsNullOrEmpty()) { @@ -406,11 +408,11 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - if (CurrentLocation != null) + if (ActiveStore is not null) { Color textColor = GUIStyle.ColorReputationNeutral; string sign = ""; - int reputationModifier = (int)MathF.Round((CurrentLocation.GetStoreReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); + int reputationModifier = (int)MathF.Round((ActiveStore.GetReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); if (reputationModifier > 0) { textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh; @@ -727,7 +729,7 @@ namespace Barotrauma ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - CurrentLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); + newLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); } } @@ -855,6 +857,28 @@ namespace Barotrauma FilterStoreItems(category, searchBox.Text); } + private static KeyValuePair? GetReputationRequirement(PriceInfo priceInfo) + { + return GameMain.GameSession?.Campaign is not null + ? priceInfo.MinReputation.FirstOrNull() + : null; + } + + private static KeyValuePair? GetTooLowReputation(PriceInfo priceInfo) + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + foreach (var minRep in priceInfo.MinReputation) + { + if (campaign.GetReputation(minRep.Key) < minRep.Value) + { + return minRep; + } + } + } + return null; + } + int prevDailySpecialCount, prevRequestedGoodsCount, prevSubRequestedGoodsCount; private void RefreshStoreBuyList() @@ -898,6 +922,7 @@ namespace Barotrauma { if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy()) { + bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : @@ -922,7 +947,8 @@ namespace Barotrauma SetOwnedText(itemFrame); SetPriceGetters(itemFrame, true); } - SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); + + SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && !GetTooLowReputation(priceInfo).HasValue); existingItemFrames.Add(itemFrame); } } @@ -1317,6 +1343,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = itemX.ItemPrefab.Name != itemY.ItemPrefab.Name ? itemX.ItemPrefab.Name.CompareTo(itemY.ItemPrefab.Name) : itemX.ItemPrefab.Identifier.CompareTo(itemY.ItemPrefab.Identifier); @@ -1345,6 +1373,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = ActiveStore.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( ActiveStore.GetAdjustedItemSellPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } @@ -1369,6 +1399,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = ActiveStore.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( ActiveStore.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } @@ -1391,10 +1423,12 @@ namespace Barotrauma specialsGroup.Recalculate(); } - static int CompareByCategory(RectTransform x, RectTransform y) + int CompareByCategory(RectTransform x, RectTransform y) { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } return itemX.ItemPrefab.Category.CompareTo(itemY.ItemPrefab.Category); } else @@ -1424,6 +1458,19 @@ namespace Barotrauma } } + int CompareByReputationRestriction(PurchasedItem item1, PurchasedItem item2) + { + PriceInfo priceInfo1 = item1.ItemPrefab.GetPriceInfo(ActiveStore); + PriceInfo priceInfo2 = item2.ItemPrefab.GetPriceInfo(ActiveStore); + if (priceInfo1 != null && priceInfo2 != null) + { + var requiredReputation1 = GetTooLowReputation(priceInfo1)?.Value ?? 0.0f; + var requiredReputation2 = GetTooLowReputation(priceInfo2)?.Value ?? 0.0f; + return requiredReputation1.CompareTo(requiredReputation2); + } + return 0; + } + static int CompareByElement(RectTransform x, RectTransform y) { if (ShouldBeOnTop(x) || ShouldBeOnBottom(y)) @@ -1753,7 +1800,7 @@ namespace Barotrauma { if (pi.ItemPrefab?.InventoryIcon != null) { - icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f: 0.5f); + icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f : 0.5f); } else if (pi.ItemPrefab?.Sprite != null) { @@ -1858,7 +1905,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.GetTooltip(); + toolTip = purchasedItem.ItemPrefab.GetTooltip(Character.Controlled); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) @@ -1871,6 +1918,23 @@ namespace Barotrauma toolTip += $"\n{TextManager.GetWithVariable("campaignstore.ownedtotal", "[amount]", itemQuantity.Total.ToString())}"; } } + + PriceInfo priceInfo = purchasedItem.ItemPrefab.GetPriceInfo(ActiveStore); + var campaign = GameMain.GameSession?.Campaign; + if (priceInfo != null && campaign != null) + { + var requiredReputation = GetReputationRequirement(priceInfo); + if (requiredReputation != null) + { + var repStr = TextManager.GetWithVariables( + "campaignstore.reputationrequired", + ("[amount]", ((int)requiredReputation.Value.Value).ToString()), + ("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value)); + Color color = campaign.GetReputation(requiredReputation.Value.Key) < requiredReputation.Value.Value ? + GUIStyle.Orange : GUIStyle.Green; + toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖"; + } + } } itemComponent.ToolTip = RichString.Rich(toolTip); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index ed1740549..d1ea71205 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -15,8 +15,6 @@ namespace Barotrauma private int pageCount; private readonly bool transferService, purchaseService; private bool initialized; - private int deliveryFee; - private string deliveryLocationName; public GUIFrame GuiFrame; private GUIFrame pageIndicatorHolder; @@ -34,14 +32,13 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, selectedSubText, switchText, missingPreviewText, currencyName; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, selectedSubText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; private readonly LocalizedString[] messageBoxOptions; - public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; private static readonly Color indicatorColor = new Color(112, 149, 129); @@ -108,14 +105,9 @@ namespace Barotrauma { initialized = true; selectedSubText = TextManager.Get("selectedsub"); - deliveryText = TextManager.Get("requestdeliverybutton"); switchText = TextManager.Get("switchtosubmarinebutton"); purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); purchaseOnlyText = TextManager.Get("purchase"); - if (transferService) - { - deliveryFee = CalculateDeliveryFee(); - } currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); @@ -124,13 +116,6 @@ namespace Barotrauma CreateGUI(); } - private int CalculateDeliveryFee() - { - int distanceToOutpost = GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - deliveryLocationName = endLocation.Name; - return DeliveryFeePerDistanceTravelled * distanceToOutpost; - } - private void CreateGUI() { createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); @@ -194,7 +179,7 @@ namespace Barotrauma confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); transferInfoFrameWidth -= confirmButtonAlt.RectTransform.RelativeSize.X; } - confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : switchText, style: "GUIButtonFreeScale"); SetConfirmButtonState(false); transferInfoFrameWidth -= confirmButton.RectTransform.RelativeSize.X; GUIFrame transferInfoFrame = new GUIFrame(new RectTransform(new Vector2(transferInfoFrameWidth, 1.0f), bottomContainer.RectTransform), style: null) @@ -406,22 +391,14 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.Price); + LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.GetPrice()); submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("price", "[amount]", amountString); } else { if (subToDisplay.Name != CurrentOrPendingSubmarine().Name) { - if (deliveryFee > 0) - { - LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); - submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("deliveryfee", "[amount]", amountString); - } - else - { - submarineDisplays[i].submarineFee.Text = string.Empty; - } + submarineDisplays[i].submarineFee.Text = string.Empty; } else { @@ -581,7 +558,7 @@ namespace Barotrauma if (owned) { - confirmButton.Text = deliveryFee > 0 ? deliveryText : switchText; + confirmButton.Text = switchText; confirmButton.OnClicked = (button, userData) => { ShowTransferPrompt(); @@ -702,37 +679,12 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (!GameMain.GameSession.Campaign.CanAfford(deliveryFee) && deliveryFee > 0) - { - new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", - ("[currencyname]", currencyName), - ("[submarinename]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); - return; - } + var text = TextManager.GetWithVariables("switchsubmarinetext", + ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - GUIMessageBox msgBox; - - if (deliveryFee > 0) - { - msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", - ("[submarinename1]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", currencyName)), messageBoxOptions); - msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; - } - else - { - var text = TextManager.GetWithVariables("switchsubmarinetext", - ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)); - text += GetItemTransferText(); - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { @@ -777,7 +729,7 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -797,7 +749,9 @@ namespace Barotrauma private void ShowBuyPrompt(bool purchaseOnly) { - if (!GameMain.GameSession.Campaign.CanAfford(selectedSubmarine.Price)) + int price = selectedSubmarine.GetPrice(); + + if (!GameMain.GameSession.Campaign.CanAfford(price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", ("[currencyname]", currencyName), @@ -810,7 +764,7 @@ namespace Barotrauma { var text = TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), - ("[amount]", selectedSubmarine.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", currencyName), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)); text += GetItemTransferText(); @@ -854,7 +808,7 @@ namespace Barotrauma if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -868,7 +822,7 @@ namespace Barotrauma { msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", ("[submarinename]", selectedSubmarine.DisplayName), - ("[amount]", selectedSubmarine.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", currencyName)) + '\n' + TextManager.Get("submarineswitchinstruction"), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 75219550f..6b8ccfbb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -836,7 +836,7 @@ namespace Barotrauma Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}"); campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => { - if (!(e.Owner is Some { Value: var owner }) || owner != character) { return; } + if (!e.Owner.TryUnwrap(out var owner) || owner != character) { return; } SetWalletText(walletBlock, e.Wallet, icon, largeIcon); }); registeredEvents.Add(eventIdentifier); @@ -1502,27 +1502,9 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(10) }; - 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.Type.Name, font: GUIStyle.SubHeadingFont); - - var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); - var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)Level.Loaded.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") - { - CanBeFocused = false - }; - - int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; - Sprite portrait = location.Type.GetPortrait(location.PortraitId); bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0; int contentWidth = missionFrameContent.Rect.Width; - if (hasPortrait) { float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; @@ -1534,6 +1516,30 @@ 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.Type.Name, font: GUIStyle.SubHeadingFont); + + if (location.Faction?.Prefab != null) + { + var factionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight); + } + var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); + var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)Level.Loaded.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight); + + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") + { + CanBeFocused = false + }; + + int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; + + GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); missionList.ContentBackground.Color = Color.Transparent; missionList.Spacing = GUI.IntScale(15); @@ -1545,6 +1551,7 @@ namespace Barotrauma foreach (Mission mission in GameMain.GameSession.Missions) { + if (!mission.Prefab.ShowInMenus) { continue; } GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) { @@ -1556,7 +1563,7 @@ namespace Barotrauma descriptionText += "\n\n" + missionMessage; } RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); - RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + RichString reputationText = mission.GetReputationRewardText(); Func wrapMissionText(GUIFont font) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 9b5318e30..6f440255e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -727,7 +727,7 @@ namespace Barotrauma talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents)); } - TalentStages collectiveStage = talentStages.Any(static stage => stage is Locked) + TalentStages collectiveStage = talentStages.All(static stage => stage is Locked) ? Locked : Available; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3a2e531c1..cff4b0a2f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Barotrauma.Extensions; @@ -77,6 +78,8 @@ namespace Barotrauma private PlayerBalanceElement? playerBalanceElement; + private static ImmutableHashSet characterList = ImmutableHashSet.Empty; + /// /// While set to true any call to will cause the buy button to be disabled and to not update the prices. /// This is to prevent us from buying another upgrade before the server has given us the new prices and causing potential syncing issues. @@ -102,6 +105,7 @@ namespace Barotrauma public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) { WaitForServerUpdate = false; + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); this.campaignUI = campaignUI; GUIFrame upgradeFrame = new GUIFrame(rectT(1, 1, parent, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { @@ -130,6 +134,7 @@ namespace Barotrauma private void RefreshAll() { + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); switch (selectedUpgradeTab) { case UpgradeTab.Repairs: @@ -273,7 +278,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); - GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, GUI.IsFourByThree() ? 0.98f : 0.95f, parent, Anchor.Center), style: null); + GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, 0.95f, parent, Anchor.Center), style: null); mainStoreLayout = new GUILayoutGroup(rectT(1, 0.9f, paddedLayout, Anchor.BottomLeft), isHorizontal: true) { RelativeSpacing = 0.01f }; topHeaderLayout = new GUILayoutGroup(rectT(1, 0.1f, paddedLayout, Anchor.TopLeft), isHorizontal: true); @@ -295,8 +300,8 @@ namespace Barotrauma new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true); categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; - GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; - GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; + GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; + GUIButton repairButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; /* RIGHT HEADER LAYOUT * |---------------------------------------------------------------------------------------------------| @@ -347,12 +352,15 @@ namespace Barotrauma SelectTab(UpgradeTab.Upgrade); - var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.27f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(GUI.IsFourByThree() ? 0.5f : 0.47f, 0.0f) }, DrawItemSwapPreview) + var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.25f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.52f * GUI.AspectRatioAdjustment, 0.0f) }, DrawItemSwapPreview) { IgnoreLayoutGroups = true, CanBeFocused = true }; + GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, repairButton.TextBlock); + #if DEBUG // creates a button that re-creates the UI CreateRefreshButton(); @@ -725,7 +733,7 @@ namespace Barotrauma if (storeLayout == null || mainStoreLayout == null) { return; } currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout)); - selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; + selectedUpgradeCategoryLayout = new GUIFrame(rectT(0.3f * GUI.AspectRatioAdjustment, 1, mainStoreLayout), style: null) { CanBeFocused = false }; RefreshUpgradeList(); @@ -956,7 +964,7 @@ namespace Barotrauma bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; if (isUninstallPending) { canUninstall = false; } - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), currentOrPending.UpgradePreviewSprite, item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame); @@ -996,7 +1004,7 @@ namespace Barotrauma int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, price, replacement, addBuyButton: true, addProgressBar: false, @@ -1102,7 +1110,7 @@ namespace Barotrauma public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } @@ -1129,7 +1137,8 @@ namespace Barotrauma GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + var name = new GUITextBlock(rectT(1, 0.35f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + //name.RectTransform.MinSize = new Point(0, (int)name.TextSize.Y); GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup? progressLayout = null; @@ -1171,7 +1180,7 @@ namespace Barotrauma materialCostList.Visible = false; materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; - var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Right) + var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.CenterRight) { UserData = UpgradeStoreUserData.PriceLabel, //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades @@ -1258,7 +1267,7 @@ namespace Barotrauma { LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), - ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); + ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1673,7 +1682,7 @@ namespace Barotrauma GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true); priceLabel.Visible = true; - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); if (!WaitForServerUpdate) { @@ -1713,7 +1722,7 @@ namespace Barotrauma static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel) { list.Content.ClearChildren(); - List allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List(); + var allItems = CargoManager.FindAllItemsOnPlayerAndSub(Character.Controlled); var resources = prefab.GetApplicableResources(targetLevel); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 6afb4c50f..3a217bf1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -163,6 +163,7 @@ namespace Barotrauma private void SetSubmarineVotingText(Client starter, SubmarineInfo info, bool transferItems, VoteType type) { + int price = info.GetPrice(); string name = starter.Name; JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; @@ -177,35 +178,21 @@ namespace Barotrauma text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), - ("[amount]", info.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.PurchaseSub: text = TextManager.GetWithVariables("submarinepurchasevote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString), - ("[amount]", info.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - if (deliveryFee > 0) - { - tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString), - ("[locationname]", endLocation.Name), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", TextManager.Get("credit").ToLower())); - } - else - { - tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString)); - } + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString)); break; } votingOnText = RichString.Rich(text); @@ -218,6 +205,7 @@ namespace Barotrauma private LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, int yesVoteCount, int noVoteCount, bool votePassed) { + int price = info.GetPrice(); LocalizedString result = string.Empty; switch (type) @@ -225,7 +213,7 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", price)), ("[currencyname]", TextManager.Get("credit").ToLower()), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]" , noVoteCount.ToString())); @@ -233,31 +221,16 @@ namespace Barotrauma case VoteType.PurchaseSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", price)), ("[currencyname]", TextManager.Get("credit").ToLower()), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]", noVoteCount.ToString())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - - if (deliveryFee > 0) - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", - ("[submarinename]", info.DisplayName), - ("[locationname]", endLocation.Name), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", deliveryFee)), - ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } - else - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", - ("[submarinename]", info.DisplayName), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", + ("[submarinename]", info.DisplayName), + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); break; default: break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index a730310d6..0df30ed06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -232,9 +232,8 @@ namespace Barotrauma } GameSettings.Init(); + CreatureMetrics.Init(); - Md5Hash.Cache.Load(); - ConsoleArguments = args; try @@ -747,7 +746,7 @@ namespace Barotrauma } else if (HasLoaded) { - if (ConnectCommand is Some { Value: var connectCommand }) + if (ConnectCommand.TryUnwrap(out var connectCommand)) { if (Client != null) { @@ -1069,21 +1068,23 @@ namespace Barotrauma public static void QuitToMainMenu(bool save) { + CreatureMetrics.Save(); if (save) { GUI.SetSavingIndicatorState(true); - if (GameSession.Submarine != null && !GameSession.Submarine.Removed) { GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); } - - // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) + if (GameSession.Campaign is CampaignMode campaign) { - spCampaign.UpdateStoreStock(); + if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) + { + spCampaign.UpdateStoreStock(); + } + GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + campaign.End(); } - SaveUtil.SaveGame(GameSession.SavePath); } @@ -1174,6 +1175,7 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { exiting = true; + CreatureMetrics.Save(); DebugConsole.NewMessage("Exiting..."); Client?.Quit(); SteamManager.ShutDown(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 7a09abd5e..eddd741f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -194,7 +193,7 @@ namespace Barotrauma }; } - var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -377,6 +376,7 @@ namespace Barotrauma - (0.1f * iconRelativeWidth) // Spacing - (7 * layoutGroup.RelativeSpacing); + nameRelativeWidth = Math.Max(nameRelativeWidth, 0.25f); var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font; var nameBlock = new GUITextBlock( @@ -1403,8 +1403,7 @@ namespace Barotrauma bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode)); - bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton is MouseButton mouseButton && - (mouseButton == MouseButton.PrimaryMouse || mouseButton == (PlayerInput.MouseButtonsSwapped() ? MouseButton.RightMouse : MouseButton.LeftMouse)); + bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton == MouseButton.PrimaryMouse; bool canToggleInterface = !isBoundToPrimaryMouse || (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode)); @@ -2796,8 +2795,8 @@ namespace Barotrauma var orderName = GetOrderNameBasedOnContextuality(order); var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color, tooltip: !showAssignmentTooltip ? orderName : orderName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); if (disableNode) { @@ -2999,8 +2998,8 @@ namespace Barotrauma var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); } if (!CanCharacterBeHeard()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs index 8e0430760..3e60238ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using System.Collections.Immutable; using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { @@ -11,21 +9,22 @@ namespace Barotrauma { private const int MaxDrawnElements = 12; - public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, int debugDrawMetadataOffset, string[] ignoredMetadataInfo) + public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, CampaignMode campaign, GUI.DebugDrawMetaData debugDrawMetaData) { var campaignData = data; - foreach (string ignored in ignoredMetadataInfo) + if (!debugDrawMetaData.FactionMetadata) { removeData("reputation.faction"); } + if (!debugDrawMetaData.UpgradeLevels) { removeData("upgrade."); } + if (!debugDrawMetaData.UpgradePrices) { removeData("upgradeprice."); } + + void removeData(string keyStartsWith) { - if (!string.IsNullOrWhiteSpace(ignored)) - { - campaignData = campaignData.Where(pair => !pair.Key.StartsWith(ignored)).ToDictionary(i => i.Key, i => i.Value); - } + campaignData = campaignData.Where(pair => !pair.Key.StartsWith(keyStartsWith)).ToDictionary(i => i.Key, i => i.Value); } int offset = 0;; if (campaignData.Count > 0) { - offset = debugDrawMetadataOffset % campaignData.Count; + offset = debugDrawMetaData.Offset % campaignData.Count; if (offset < 0) { offset += campaignData.Count; } } @@ -72,7 +71,7 @@ namespace Barotrauma } float y = infoRect.Bottom + 16; - if (Campaign.Factions != null) + if (campaign.Factions != null) { const string factionHeader = "Reputations"; Vector2 factionHeaderSize = GUIStyle.SubHeadingFont.MeasureString(factionHeader); @@ -81,7 +80,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUIStyle.SubHeadingFont); y += factionHeaderSize.Y + 8; - foreach (Faction faction in Campaign.Factions) + foreach (Faction faction in campaign.Factions) { LocalizedString name = faction.Prefab.Name; Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); @@ -94,20 +93,6 @@ namespace Barotrauma y += 15; } } - - Location location = Campaign.Map?.CurrentLocation; - if (location?.Reputation != null) - { - string name = Campaign.Map?.CurrentLocation.Name; - Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); - y += nameSize.Y + 5; - - float normalizedReputation = MathUtils.InverseLerp(location.Reputation.MinReputation, location.Reputation.MaxReputation, location.Reputation.Value); - Color color = ToolBox.GradientLerp(normalizedReputation, Color.Red, Color.Yellow, Color.LightGreen); - GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(normalizedReputation * 255), 10), color, isFilled: true); - GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); - } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs index 4a91c9026..d004cd9bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -13,7 +13,7 @@ namespace Barotrauma partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { - if (Owner is Some { Value: var character }) + if (Owner.TryUnwrap(out var character)) { if (!character.IsPlayer) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 3aae45f01..d893dc526 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -16,8 +15,6 @@ namespace Barotrauma protected bool crewDead; protected Color overlayColor; - protected LocalizedString overlayText, overlayTextBottom; - protected Color overlayTextColor; protected Sprite overlaySprite; private TransitionType prevCampaignUIAutoOpenType; @@ -30,7 +27,13 @@ namespace Barotrauma protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; - public static CancellationTokenSource StartRoundCancellationToken { get; private set; } + public SlideshowPlayer SlideshowPlayer + { + get; + protected set; + } + + private CancellationTokenSource startRoundCancellationToken; public bool ForceMapUI { @@ -59,10 +62,19 @@ namespace Barotrauma { chatBox.ToggleOpen = wasChatBoxOpen; } - if (!value && CampaignUI?.SelectedTab == InteractionType.PurchaseSub) + if (!value) { - SubmarinePreview.Close(); + switch (CampaignUI?.SelectedTab) + { + case InteractionType.PurchaseSub: + SubmarinePreview.Close(); + break; + case InteractionType.MedicalClinic: + CampaignUI.MedicalClinic?.OnDeselected(); + break; + } } + showCampaignUI = value; } } @@ -77,6 +89,7 @@ namespace Barotrauma { foreach (Mission mission in Missions.ToList()) { + if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox( RichString.Rich(mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) @@ -108,6 +121,23 @@ namespace Barotrauma { return AllowedToManageCampaign(ClientPermissions.ManageMoney); } + protected GUIButton CreateEndRoundButton() + { + int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f)); + int buttonHeight = (int)(40 * GUI.yScale); + var rectT = HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y, buttonWidth, buttonHeight), GUI.Canvas); + rectT.Pivot = Pivot.Center; + return new GUIButton(rectT, TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + { + Pulse = true, + TextBlock = + { + Shadow = true, + AutoScaleHorizontal = true + } + }; + } + public override void Draw(SpriteBatch spriteBatch) { @@ -123,32 +153,10 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); } - if (!overlayText.IsNullOrEmpty() && overlayTextColor.A > 0) - { - var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); - Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; - LocalizedString wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUIStyle.Font); - Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); - Vector2 textPos = centerPos - textSize / 2; - backgroundSprite.Draw(spriteBatch, - centerPos, - Color.White * (overlayTextColor.A / 255.0f), - origin: backgroundSprite.size / 2, - rotate: 0.0f, - scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 1.5f)); - - GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); - - if (!overlayTextBottom.IsNullOrEmpty()) - { - Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(overlayTextBottom) / 2; - GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom.Value, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom.Value, overlayTextColor); - } - } } + SlideshowPlayer?.DrawManually(spriteBatch); + if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) { endRoundButton.Visible = false; @@ -157,7 +165,7 @@ namespace Barotrauma } if (Submarine.MainSub == null || Level.Loaded == null) { return; } - endRoundButton.Visible = false; + bool allowEndingRound = false; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); LocalizedString buttonText = ""; switch (availableTransition) @@ -168,12 +176,12 @@ namespace Barotrauma { string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; break; case TransitionType.ReturnToPreviousLocation: case TransitionType.ReturnToPreviousEmptyLocation: @@ -181,31 +189,27 @@ namespace Barotrauma { string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } - break; case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && + !Level.Loaded.IsEndBiome && (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; - } - else - { - endRoundButton.Visible = false; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; } if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) { - endRoundButton.Visible = false; + allowEndingRound = false; } + if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound; } - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } - + endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false }; if (endRoundButton.Visible) { if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) @@ -259,11 +263,11 @@ namespace Barotrauma GUI.ClearCursorWait(); - StartRoundCancellationToken = new CancellationTokenSource(); + startRoundCancellationToken = new CancellationTokenSource(); var loadTask = Task.Run(async () => { await Task.Yield(); - Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; + Rand.ThreadId = Environment.CurrentManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); @@ -273,7 +277,8 @@ namespace Barotrauma roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; - }, StartRoundCancellationToken.Token); + startRoundCancellationToken = null; + }, startRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; @@ -283,6 +288,21 @@ namespace Barotrauma return loadTask; } + public void CancelStartRound() + { + startRoundCancellationToken?.Cancel(); + } + + public void ThrowIfStartRoundCancellationRequested() + { + if (startRoundCancellationToken != null && + startRoundCancellationToken.Token.IsCancellationRequested) + { + startRoundCancellationToken.Token.ThrowIfCancellationRequested(); + startRoundCancellationToken = null; + } + } + protected SubmarineInfo GetPredefinedStartOutpost() { if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) @@ -316,10 +336,15 @@ namespace Barotrauma goto default; default: ShowCampaignUI = true; - CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); + CampaignUI.SelectTab(npc.CampaignInteractionType, npc); CampaignUI.UpgradeStore?.RequestRefresh(); break; } + + if (npc.AIController is HumanAIController humanAi && humanAi.IsInHostileFaction()) + { + npc.Speak(TextManager.Get("dialoglowrepcampaigninteraction").Value, identifier: "dialoglowrepcampaigninteraction".ToIdentifier(), minDurationBetweenSimilar: 60.0f); + } } public override void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 470e9be68..c7fb14619 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -125,38 +125,25 @@ namespace Barotrauma private void CreateButtons() { - int buttonHeight = (int) (GUI.Scale * 40), - buttonWidth = GUI.IntScale(450), - buttonCenter = buttonHeight / 2, - screenMiddle = GameMain.GraphicsWidth / 2; - - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUI.Canvas), - TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + endRoundButton = CreateEndRoundButton(); + endRoundButton.OnClicked = (btn, userdata) => { - Pulse = true, - TextBlock = - { - Shadow = true, - AutoScaleHorizontal = true - }, - OnClicked = (btn, userdata) => - { - TryEndRoundWithFuelCheck( - onConfirm: () => GameMain.Client.RequestStartRound(), - onReturnToMapScreen: () => - { - ShowCampaignUI = true; - if (CampaignUI == null) { InitCampaignUI(); } - CampaignUI.SelectTab(InteractionType.Map); - }); - return true; - } + TryEndRoundWithFuelCheck( + onConfirm: () => GameMain.Client.RequestStartRound(), + onReturnToMapScreen: () => + { + ShowCampaignUI = true; + if (CampaignUI == null) { InitCampaignUI(); } + CampaignUI.SelectTab(InteractionType.Map); + }); + return true; }; - int readyButtonHeight = buttonHeight; - int readyButtonWidth = (int) (GUI.Scale * 50); - - ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), + int readyButtonWidth = (int)(GUI.Scale * 50 * (GUI.IsUltrawide ? 3.0f : 1.0f)); + int readyButtonHeight = (int)(GUI.Scale * 40); + int readyButtonCenter = readyButtonHeight / 2, + screenMiddle = GameMain.GraphicsWidth / 2; + ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (endRoundButton.Rect.Width / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - readyButtonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), style: "RepairBuyButton") { ToolTip = TextManager.Get("ReadyCheck.Tooltip"), @@ -216,51 +203,35 @@ namespace Barotrauma } Character prevControlled = Character.Controlled; - if (prevControlled?.AIController != null) - { - prevControlled.AIController.Enabled = false; - } GUI.DisableHUD = true; if (IsFirstRound) { - Character.Controlled = null; + if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) + { + SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); + } + Character.Controlled = null; prevControlled?.ClearInputs(); overlayColor = Color.LightGray; overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); - overlayTextColor = Color.Transparent; - overlayText = TextManager.GetWithVariables("campaignstart", - ("xxxx", Map.CurrentLocation.Name), ("yyyy", TextManager.Get($"submarineclass.{Submarine.MainSub.Info.SubmarineClass}"))); - float fadeInDuration = 1.0f; - float textDuration = 10.0f; - float timer = 0.0f; - while (timer < textDuration) - { - if (GameMain.GameSession == null || Screen.Selected != GameMain.GameScreen) - { - GUI.DisableHUD = false; - yield return CoroutineStatus.Success; - } - // Try to grab the controlled here to prevent inputs, assigned late on multiplayer - if (Character.Controlled != null) - { - prevControlled = Character.Controlled; - Character.Controlled = null; - prevControlled.ClearInputs(); - } - GameMain.GameScreen.Cam.Freeze = true; - overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); - yield return CoroutineStatus.Running; - } + var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); borders.Location += outpost.WorldPosition.ToPoint(); GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); - GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) + { + GUI.PreventPauseMenuToggle = true; + yield return CoroutineStatus.Running; + } + GUI.PreventPauseMenuToggle = false; + prevControlled ??= Character.Controlled; + GameMain.LightManager.LosAlpha = 0.0f; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, @@ -272,16 +243,6 @@ namespace Barotrauma AllowInterrupt = true, RemoveControlFromCharacter = false }; - fadeInDuration = 1.0f; - timer = 0.0f; - overlayTextColor = Color.Transparent; - overlayText = ""; - while (timer < fadeInDuration) - { - overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; - yield return CoroutineStatus.Running; - } overlayColor = Color.Transparent; while (transition.Running) { @@ -385,7 +346,7 @@ namespace Barotrauma overlayColor = Color.Transparent; if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); } - if (!(Screen.Selected is RoundSummaryScreen)) + if (Screen.Selected is not RoundSummaryScreen) { if (continueButton != null) { @@ -409,6 +370,8 @@ namespace Barotrauma base.Update(deltaTime); + SlideshowPlayer?.UpdateManually(deltaTime); + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { @@ -442,7 +405,8 @@ namespace Barotrauma CampaignUI.SelectTab(InteractionType.Map); } } - else + //end biome is handled by the server (automatic transition without a map screen when the end of the level is reached) + else if (!Level.Loaded.IsEndBiome) { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost @@ -467,11 +431,17 @@ namespace Barotrauma } } + public override void UpdateWhilePaused(float deltaTime) + { + SlideshowPlayer?.UpdateManually(deltaTime); + } + public override void End(TransitionType transitionType = TransitionType.None) { base.End(transitionType); ForceMapUI = ShowCampaignUI = false; - + SlideshowPlayer?.Finish(); + // remove all event dialogue boxes GUIMessageBox.MessageBoxes.ForEachMod(mb => { @@ -501,7 +471,8 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { GameMain.NetLobbyScreen.Select(); @@ -510,32 +481,6 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() - { - Character controlled = Character.Controlled; - if (controlled != null) - { - controlled.AIController.Enabled = false; - } - - GUI.DisableHUD = true; - ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); - var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, - null, Alignment.Center, - fadeOut: true, - panDuration: 10, - startZoom: null, endZoom: 0.2f); - - while (transition.Running) - { - yield return CoroutineStatus.Running; - } - GameMain.CampaignEndScreen.Select(); - GUI.DisableHUD = false; - - yield return CoroutineStatus.Success; - } - public void ClientWrite(IWriteMessage msg) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); @@ -844,8 +789,6 @@ namespace Barotrauma { DebugConsole.Log("Received campaign update (Reputation)"); UInt16 id = msg.ReadUInt16(); - float? reputation = null; - if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } Dictionary factionReps = new Dictionary(); byte factionsCount = msg.ReadByte(); for (int i = 0; i < factionsCount; i++) @@ -854,11 +797,6 @@ namespace Barotrauma } if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) { - if (reputation.HasValue) - { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } foreach (var (identifier, rep) in factionReps) { Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); @@ -871,6 +809,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); } } + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } } if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) @@ -995,7 +934,9 @@ namespace Barotrauma if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } - if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) + if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null && + /*can't apply until we have the latest save file*/ + !NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } @@ -1010,25 +951,20 @@ namespace Barotrauma foreach (NetWalletTransaction transaction in update.Transactions) { WalletInfo info = transaction.Info; - switch (transaction.CharacterID) + if (transaction.CharacterID.TryUnwrap(out var charID)) { - case Some { Value: var charID }: - { - Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); - if (targetCharacter is null) { break; } - Wallet wallet = targetCharacter.Wallet; + Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); + if (targetCharacter is null) { break; } + Wallet wallet = targetCharacter.Wallet; - wallet.Balance = info.Balance; - wallet.RewardDistribution = info.RewardDistribution; - TryInvokeEvent(wallet, transaction.ChangedData, info); - break; - } - case None _: - { - Bank.Balance = info.Balance; - TryInvokeEvent(Bank, transaction.ChangedData, info); - break; - } + wallet.Balance = info.Balance; + wallet.RewardDistribution = info.RewardDistribution; + TryInvokeEvent(wallet, transaction.ChangedData, info); + } + else + { + Bank.Balance = info.Balance; + TryInvokeEvent(Bank, transaction.ChangedData, info); } } @@ -1043,7 +979,7 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { - if (!AllowedToManageCampaign(ClientPermissions.ManageCampaign)) + if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) { return PersonalWallet.TryDeduct(price); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 722297b66..d2c611e10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -12,7 +12,13 @@ namespace Barotrauma public override bool Paused { - get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map; } + get + { + return + ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || + ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map || + (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown); + } } public override void UpdateWhilePaused(float deltaTime) @@ -31,6 +37,8 @@ namespace Barotrauma } } + SlideshowPlayer?.UpdateManually(deltaTime); + CrewManager.ChatBox?.Update(deltaTime); CrewManager.UpdateReports(); } @@ -77,9 +85,9 @@ namespace Barotrauma /// private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { - CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); Settings = settings; + InitFactions(); map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { @@ -89,7 +97,6 @@ namespace Barotrauma CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } - InitCampaignData(); InitUI(); } @@ -99,6 +106,17 @@ namespace Barotrauma private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "metadata": + CampaignMetadata.Load(subElement); + break; + } + } + + InitFactions(); foreach (var subElement in element.Elements()) { @@ -114,9 +132,6 @@ namespace Barotrauma case "map": map = Map.Load(this, subElement); break; - case "metadata": - CampaignMetadata = new CampaignMetadata(this, subElement); - break; case "cargo": CargoManager.LoadPurchasedItems(subElement); break; @@ -133,14 +148,14 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case "eventmanager": + GameMain.GameSession.EventManager.Load(subElement); + break; } } - CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); - InitUI(); //backwards compatibility for saves made prior to the addition of personal wallets @@ -198,28 +213,14 @@ namespace Barotrauma { StartRound = () => { TryEndRound(); } }; - } - private void CreateEndRoundButton() - { - int buttonHeight = (int)(GUI.Scale * 40); - int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas), - TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + endRoundButton = CreateEndRoundButton(); + endRoundButton.OnClicked = (btn, userdata) => { - Pulse = true, - TextBlock = - { - Shadow = true, - AutoScaleHorizontal = true - }, - OnClicked = (btn, userdata) => - { - TryEndRoundWithFuelCheck( - onConfirm: () => TryEndRound(), - onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); - return true; - } + TryEndRoundWithFuelCheck( + onConfirm: () => TryEndRound(), + onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); + return true; }; } @@ -265,7 +266,6 @@ namespace Barotrauma private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { - GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); @@ -296,34 +296,9 @@ namespace Barotrauma if (IsFirstRound || showCampaignResetText) { - overlayColor = Color.LightGray; - overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); - overlayTextColor = Color.Transparent; - overlayText = TextManager.GetWithVariables(showCampaignResetText ? "campaignend4" : "campaignstart", - ("xxxx", Map.CurrentLocation.Name), - ("yyyy", TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass))); - LocalizedString pressAnyKeyText = TextManager.Get("pressanykey"); - float fadeInDuration = 2.0f; - float textDuration = 10.0f; - float timer = 0.0f; - while (true) + if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) { - if (timer > fadeInDuration) - { - overlayTextBottom = pressAnyKeyText; - if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0 || PlayerInput.PrimaryMouseButtonClicked()) - { - break; - } - } - if (GameMain.GameSession == null) - { - GUI.DisableHUD = false; - yield return CoroutineStatus.Success; - } - overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); - yield return CoroutineStatus.Running; + SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); } var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); @@ -331,7 +306,13 @@ namespace Barotrauma GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); - GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) + { + GUI.PreventPauseMenuToggle = true; + yield return CoroutineStatus.Running; + } + GUI.PreventPauseMenuToggle = false; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, @@ -343,17 +324,6 @@ namespace Barotrauma AllowInterrupt = true, RemoveControlFromCharacter = false }; - fadeInDuration = 1.0f; - timer = 0.0f; - overlayTextColor = Color.Transparent; - overlayText = ""; - while (timer < fadeInDuration) - { - overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; - yield return CoroutineStatus.Running; - } - overlayColor = Color.Transparent; while (transition.Running) { yield return CoroutineStatus.Running; @@ -440,61 +410,68 @@ namespace Barotrauma TotalPassedLevels++; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; + case TransitionType.End: + EndCampaign(); + IsFirstRound = true; + break; } - Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); - - var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, - transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, - fadeOut: false, - panDuration: EndTransitionDuration); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); GUI.ClearMessages(); - Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; - overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); - float fadeOutDuration = endTransition.PanDuration; - float t = 0.0f; - while (t < fadeOutDuration || endTransition.Running) - { - t += CoroutineManager.DeltaTime; - overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); - yield return CoroutineStatus.Running; - } - overlayColor = Color.White; - yield return CoroutineStatus.Running; - //-------------------------------------- - - if (success) + if (transitionType != TransitionType.End) { - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - } - else - { - PendingSubmarineSwitch = null; - EnableRoundSummaryGameOverState(); - } + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, + transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, + fadeOut: false, + panDuration: EndTransitionDuration); - CrewManager?.ClearCurrentOrders(); - - //-------------------------------------- - - SelectSummaryScreen(roundSummary, newLevel, mirror, () => - { - GameMain.GameScreen.Select(); - if (continueButton != null) + Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; + overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); + float fadeOutDuration = endTransition.PanDuration; + float t = 0.0f; + while (t < fadeOutDuration || endTransition.Running) { - continueButton.Visible = true; + t += CoroutineManager.DeltaTime; + overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); + yield return CoroutineStatus.Running; + } + overlayColor = Color.White; + yield return CoroutineStatus.Running; + + //-------------------------------------- + + if (success) + { + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + PendingSubmarineSwitch = null; + EnableRoundSummaryGameOverState(); } - GUI.DisableHUD = false; - GUI.ClearCursorWait(); - overlayColor = Color.Transparent; - }); + CrewManager?.ClearCurrentOrders(); + + SelectSummaryScreen(roundSummary, newLevel, mirror, () => + { + GameMain.GameScreen.Select(); + if (continueButton != null) + { + continueButton.Visible = true; + } + + GUI.DisableHUD = false; + GUI.ClearCursorWait(); + overlayColor = Color.Transparent; + }); + } GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; @@ -502,7 +479,10 @@ namespace Barotrauma protected override void EndCampaignProjSpecific() { - CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { showCampaignResetText = true; @@ -511,39 +491,14 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() - { - if (Character.Controlled != null) - { - Character.Controlled.AIController.Enabled = false; - Character.Controlled = null; - } - GUI.DisableHUD = true; - ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); - var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, - null, Alignment.Center, - fadeOut: true, - panDuration: 10, - startZoom: null, endZoom: 0.2f); - - while (transition.Running) - { - yield return CoroutineStatus.Running; - } - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - GameMain.CampaignEndScreen.Select(); - GUI.DisableHUD = false; - - yield return CoroutineStatus.Success; - } - public override void Update(float deltaTime) { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } base.Update(deltaTime); - + + SlideshowPlayer?.UpdateManually(deltaTime); + Map?.Radiation?.UpdateRadiation(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || @@ -594,11 +549,19 @@ namespace Barotrauma CampaignUI.SelectTab(InteractionType.Map); } } + else if (Level.Loaded.IsEndBiome) + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.ProgressToNextLocation) + { + LoadNewLevel(); + } + } 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) + if (!Submarine.MainSub.AtStartExit && !Level.Loaded.StartOutpost.ExitPoints.Any()) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); @@ -608,11 +571,11 @@ namespace Barotrauma else { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); - if (transitionType == TransitionType.End) + if (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation) { - EndCampaign(); + LoadNewLevel(); } - if (transitionType == TransitionType.ProgressToNextLocation && + else if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { LoadNewLevel(); @@ -725,6 +688,11 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + if (GameMain.GameSession?.EventManager != null) + { + modeElement.Add(GameMain.GameSession?.EventManager.Save()); + } + //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index fcb5438dc..49f2a6fcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -585,7 +585,7 @@ namespace Barotrauma if (!gap.IsRoomToRoom) { if (!IsWearingDivingSuit()) { continue; } - if (Character.Controlled.IsProtectedFromPressure()) { continue; } + if (Character.Controlled.IsProtectedFromPressure) { continue; } if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 232e84838..3b4d31cf6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -11,6 +11,8 @@ namespace Barotrauma { internal sealed partial class MedicalClinic { + private MedicalClinicUI? ui => campaign?.CampaignUI?.MedicalClinic; + public enum RequestResult { Undecided, @@ -303,6 +305,12 @@ namespace Barotrauma } } + private void AfflictionUpdateReceived(IReadMessage inc) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + ui?.UpdateAfflictions(crewMember); + } + private void PendingRequestReceived(IReadMessage inc) { var pendingCrew = INetSerializableStruct.Read>(inc); @@ -312,6 +320,10 @@ namespace Barotrauma } } + public static void SendUnsubscribeRequest() => ClientSend(null, + header: NetworkHeader.UNSUBSCRIBE_ME, + deliveryMethod: DeliveryMethod.Reliable); + private static IWriteMessage StartSending() { IWriteMessage writeMessage = new WriteOnlyMessage(); @@ -337,6 +349,9 @@ namespace Barotrauma case NetworkHeader.REQUEST_AFFLICTIONS: AfflictionRequestReceived(inc); break; + case NetworkHeader.AFFLICTION_UPDATE: + AfflictionUpdateReceived(inc); + break; case NetworkHeader.REQUEST_PENDING: PendingRequestReceived(inc); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 9da10c685..8ba436270 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -40,7 +40,7 @@ namespace Barotrauma private void CreateMessageBox(string author) { - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.3f : 0.2f, 0.15f); + Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index da2c6b35b..53bcf4f1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -21,8 +21,7 @@ namespace Barotrauma private readonly GameMode gameMode; - private readonly float initialLocationReputation; - private readonly Dictionary initialFactionReputations = new Dictionary(); + private readonly Dictionary initialFactionReputations = new Dictionary(); public GUILayoutGroup ButtonArea { get; private set; } @@ -36,12 +35,11 @@ namespace Barotrauma this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; this.endLocation = endLocation; - initialLocationReputation = startLocation?.Reputation?.Value ?? 0.0f; if (gameMode is CampaignMode campaignMode) { foreach (Faction faction in campaignMode.Factions) { - initialFactionReputations.Add(faction, faction.Reputation.Value); + initialFactionReputations.Add(faction.Prefab.Identifier, faction.Reputation.Value); } } } @@ -214,11 +212,13 @@ namespace Barotrauma Stretch = true }; - List missionsToDisplay = new List(selectedMissions); - if (!selectedMissions.Any() && startLocation != null) + List missionsToDisplay = new List(selectedMissions.Where(m => m.Prefab.ShowInMenus)); + if (startLocation != null) { foreach (Mission mission in startLocation.SelectedMissions) { + if (missionsToDisplay.Contains(mission)) { continue; } + if (!mission.Prefab.ShowInMenus) { continue; } if (mission.Locations[0] == mission.Locations[1] || mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) { @@ -312,18 +312,27 @@ namespace Barotrauma } var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(missionMessage), wrap: true); - int reward = displayedMission.GetReward(Submarine.MainSub); - if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) + RichString reputationText = displayedMission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); - if (share > 0) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true); + } + + int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); + if (totalReward > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); - RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); + if (share > 0) + { + string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); + RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + } } } } @@ -401,33 +410,17 @@ namespace Barotrauma }; reputationList.ContentBackground.Color = Color.Transparent; - if (startLocation.Type.HasOutpost && startLocation.Reputation != null) - { - var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); - var locationFrame = CreateReputationElement( - reputationList.Content, - startLocation.Name, - startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, - startLocation.Type.Name, "", - iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); - CreatePathUnlockElement(locationFrame, null, startLocation); - } - foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; - if (initialFactionReputations.ContainsKey(faction)) - { - initialReputation = initialFactionReputations[faction]; - } - else + if (!initialFactionReputations.TryGetValue(faction.Prefab.Identifier, out initialReputation)) { DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); } var factionFrame = CreateReputationElement( reputationList.Content, faction.Prefab.Name, - faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, + faction.Reputation, initialReputation, faction.Prefab.ShortDescription, faction.Prefab.Description, faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); CreatePathUnlockElement(factionFrame, faction, null); @@ -455,52 +448,60 @@ namespace Barotrauma void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) { - if (GameMain.GameSession?.Campaign?.Map != null) + if (GameMain.GameSession?.Campaign?.Map == null) { return; } + + IEnumerable connectionsBetweenBiomes = + GameMain.GameSession.Campaign.Map.Connections.Where(c => c.Locations[0].Biome != c.Locations[1].Biome); + + foreach (LocationConnection connection in connectionsBetweenBiomes) { - foreach (LocationConnection connection in GameMain.GameSession.Campaign.Map.Connections) + if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + + //don't show the "reputation required to unlock" text if another connection between the biomes has already been unlocked + if (connectionsBetweenBiomes.Where(c => !c.Locked).Any(c => + (c.Locations[0].Biome == connection.Locations[0].Biome && c.Locations[1].Biome == connection.Locations[1].Biome) || + (c.Locations[1].Biome == connection.Locations[0].Biome && c.Locations[0].Biome == connection.Locations[1].Biome))) { - if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + continue; + } - var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); + var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); - if (unlockEvent == null) { continue; } - if (string.IsNullOrEmpty(unlockEvent.UnlockPathFaction) || unlockEvent.UnlockPathFaction.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (unlockEvent == null) { continue; } + if (unlockEvent.Faction.IsEmpty) + { + if (location == null || gateLocation != location) { continue; } + } + else + { + if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } + } + + if (unlockEvent != null) + { + Reputation unlockReputation = gateLocation.Reputation; + Faction unlockFaction = null; + if (!unlockEvent.Faction.IsEmpty) { - if (location == null || gateLocation != location) { continue; } + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); + unlockReputation = unlockFaction?.Reputation; } - else + float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); + RichString unlockText = RichString.Rich(TextManager.GetWithVariables( + "lockedpathreputationrequirement", + ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); + var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); + unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); + unlockInfoPanel.UserData = "unlockinfo"; + if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { - if (faction == null || faction.Prefab.Identifier != unlockEvent.UnlockPathFaction) { continue; } - } - - if (unlockEvent != null) - { - Reputation unlockReputation = gateLocation.Reputation; - Faction unlockFaction = null; - if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) - { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); - unlockReputation = unlockFaction?.Reputation; - } - float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); - RichString unlockText = RichString.Rich(TextManager.GetWithVariables( - "lockedpathreputationrequirement", - ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); - var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, - unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); - unlockInfoPanel.UserData = "unlockinfo"; - if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) - { - unlockInfoPanel.Font = GUIStyle.SmallFont; - } + unlockInfoPanel.Font = GUIStyle.SmallFont; } } - } + } } } @@ -543,6 +544,11 @@ namespace Barotrauma } } + if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) + { + locationName ??= startLocation.Name; + } + if (textTag == null) { return ""; } if (locationName == null) @@ -680,7 +686,7 @@ namespace Barotrauma } private GUIFrame CreateReputationElement(GUIComponent parent, - LocalizedString name, float reputation, float normalizedReputation, float initialReputation, + LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); @@ -698,21 +704,22 @@ namespace Barotrauma }; } - var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft, isHorizontal: true) + var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterRight, isHorizontal: true) { AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; + var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) + { + Color = iconColor + }; var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { AbsoluteSpacing = GUI.IntScale(10), Stretch = true }; - var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) - { - Color = iconColor - }; + factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), @@ -733,24 +740,30 @@ namespace Barotrauma factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue)); + + var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + SetReputationText(reputationText); + reputation?.OnReputationValueChanged.RegisterOverwriteExisting("RefreshRoundSummary".ToIdentifier(), _ => + { + SetReputationText(reputationText); + }); - LocalizedString reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); - int reputationChange = (int)Math.Round(reputation - initialReputation); - if (Math.Abs(reputationChange) > 0) + void SetReputationText(GUITextBlock textBlock) { - string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); - var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - richText, - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - RichString.Rich(reputationText), - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true); + int reputationChange = (int)Math.Round(reputation.Value - initialReputation); + if (Math.Abs(reputationChange) > 0) + { + string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; + string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + textBlock.Text = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); + } + else + { + textBlock.Text = RichString.Rich(reputationText); + } } //spacing diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 18139a698..e388ac515 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -63,7 +63,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; - public static int Spacing; private Layout layout; public Layout CurrentLayout @@ -103,7 +102,7 @@ namespace Barotrauma { visualSlots ??= new VisualSlot[capacity]; - float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; + float multiplier = UIScale * GUI.AspectRatioAdjustment; for (int i = 0; i < capacity; i++) { @@ -219,18 +218,11 @@ namespace Barotrauma private void SetSlotPositions(Layout layout) { - bool isFourByThree = GUI.IsFourByThree(); - if (isFourByThree) - { - Spacing = (int)(5 * UIScale); - } - else - { - Spacing = (int)(8 * UIScale); - } + int spacing = GUI.IntScale(5); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); - int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; + SlotSize = (SlotSpriteSmall.size * UIScale * GUI.AspectRatioAdjustment).ToPoint(); + int bottomOffset = SlotSize.Y + spacing * 2 + ContainedIndicatorHeight; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - spacing * 2 - (int)(UnequippedIndicator.size.Y * UIScale); if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -242,11 +234,11 @@ namespace Barotrauma int personalSlotCount = SlotTypes.Count(s => PersonalSlots.HasFlag(s)); int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); - int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing; + int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + spacing) / 2; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - spacing; //make sure the rightmost normal slot doesn't overlap with the personal slots - x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); + x -= Math.Max((x + normalSlotCount * (SlotSize.X + spacing)) - (upperX - personalSlotCount * (SlotSize.X + spacing)), 0); int hideButtonSlotIndex = -1; for (int i = 0; i < SlotPositions.Length; i++) @@ -254,7 +246,7 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(upperX, GameMain.GraphicsHeight - bottomOffset); - upperX -= SlotSize.X + Spacing; + upperX -= SlotSize.X + spacing; personalSlotArea = (hideButtonSlotIndex == -1) ? new Rectangle(SlotPositions[i].ToPoint(), SlotSize) : Rectangle.Union(personalSlotArea, new Rectangle(SlotPositions[i].ToPoint(), SlotSize)); @@ -263,7 +255,7 @@ namespace Barotrauma else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += SlotSize.X + Spacing; + x += SlotSize.X + spacing; } } } @@ -271,7 +263,7 @@ namespace Barotrauma case Layout.Right: { int x = HUDLayoutSettings.InventoryAreaLower.Right; - int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; + int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - spacing; for (int i = 0; i < visualSlots.Length; i++) { if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } @@ -282,19 +274,18 @@ namespace Barotrauma } else { - x -= SlotSize.X + Spacing; + x -= SlotSize.X + spacing; } } int lowerX = x; int handSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { SlotPositions[i] = new Vector2(handSlotX, personalSlotY); - handSlotX += visualSlots[i].Rect.Width + Spacing; + handSlotX += visualSlots[i].Rect.Width + spacing; continue; } @@ -302,12 +293,12 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX -= visualSlots[i].Rect.Width + Spacing; + personalSlotX -= visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } @@ -316,7 +307,7 @@ namespace Barotrauma { if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } - x -= visualSlots[i].Rect.Width + Spacing; + x -= visualSlots[i].Rect.Width + spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } @@ -325,7 +316,6 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.X; int personalSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { @@ -334,33 +324,33 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX += visualSlots[i].Rect.Width + Spacing; + personalSlotX += visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - int handSlotX = x - visualSlots[0].Rect.Width - Spacing; + int handSlotX = x - visualSlots[0].Rect.Width - spacing; for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { bool rightSlot = SlotTypes[i] == InvSlotType.RightHand; - SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); + SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - spacing, personalSlotY); continue; } if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } break; case Layout.Center: { int columns = 5; - int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + Spacing * (columns - 1)) / 2; + int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + spacing * (columns - 1)) / 2; int startY = GameMain.GraphicsHeight / 2 - (SlotSize.Y * 2); int x = startX, y = startY; for (int i = 0; i < SlotPositions.Length; i++) @@ -369,10 +359,10 @@ namespace Barotrauma if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - y += visualSlots[0].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; + y += visualSlots[0].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; x = startX; int n = 0; for (int i = 0; i < SlotPositions.Length; i++) @@ -381,12 +371,12 @@ namespace Barotrauma if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; n++; if (n >= columns) { x = startX; - y += visualSlots[i].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; + y += visualSlots[i].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; n = 0; } } @@ -402,7 +392,7 @@ namespace Barotrauma { if (SlotTypes[i] != InvSlotType.HealthInterface) { continue; } SlotPositions[i] = pos; - pos.Y += visualSlots[i].Rect.Height + Spacing; + pos.Y += visualSlots[i].Rect.Height + spacing; } } @@ -641,7 +631,7 @@ namespace Barotrauma { slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { slot.EquipButtonState = GUIComponent.ComponentState.None; } @@ -1018,7 +1008,47 @@ namespace Barotrauma SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } - + + public bool CanBeAutoMovedToCorrectSlots(Item item) + { + if (item == null) { return false; } + foreach (var allowedSlot in item.AllowedSlots) + { + InvSlotType slotsFree = InvSlotType.None; + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } + } + if (allowedSlot == slotsFree) { return true; } + } + return false; + } + + /// + /// Flash the slots the item is allowed to go in (not taking into account whether there's already something in those slots) + /// + public void FlashAllowedSlots(Item item, Color color) + { + if (item == null || visualSlots == null) { return; } + bool flashed = false; + foreach (var allowedSlot in item.AllowedSlots) + { + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i])) + { + visualSlots[i].ShowBorderHighlight(color, 0.1f, 0.9f); + flashed = true; + } + } + } + if (flashed) + { + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } + } + + public void DrawOwn(SpriteBatch spriteBatch) { if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { return; } @@ -1106,40 +1136,24 @@ namespace Barotrauma color *= 0.5f; } - if (character.HasEquippedItem(slots[i].First())) + Vector2 indicatorScale = new Vector2( + visualSlots[i].EquipButtonRect.Size.X / EquippedIndicator.size.X, + visualSlots[i].EquipButtonRect.Size.Y / EquippedIndicator.size.Y); + + bool isEquipped = character.HasEquippedItem(slots[i].First()); + var sprite = state switch { - switch (state) - { - case GUIComponent.ComponentState.None: - EquippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - EquippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - EquippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } - else - { - switch (state) - { - case GUIComponent.ComponentState.None: - UnequippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - UnequippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - UnequippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } + GUIComponent.ComponentState.None + => isEquipped ? EquippedIndicator : UnequippedIndicator, + GUIComponent.ComponentState.Hover + => isEquipped ? EquippedHoverIndicator : UnequippedHoverIndicator, + GUIComponent.ComponentState.Pressed + or GUIComponent.ComponentState.Selected + or GUIComponent.ComponentState.HoverSelected + => isEquipped ? EquippedClickedIndicator : UnequippedClickedIndicator, + _ => throw new NotImplementedException() + }; + sprite.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, indicatorScale); } if (Locked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index f5faabb79..1093798a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -92,9 +92,6 @@ namespace Barotrauma.Items.Components rect.Height = (int)(rect.Height * (1.0f - openState)); } - //only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed - //it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion - bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f; if (Window.Height > 0 && Window.Width > 0) { if (IsHorizontal) @@ -117,7 +114,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); + SetVertices(convexHull2, rect2); } } } @@ -141,7 +138,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); + SetVertices(convexHull2, rect2); } } } @@ -156,13 +153,28 @@ namespace Barotrauma.Items.Components else { convexHull.Enabled = true; - convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments); + SetVertices(convexHull, rect); } + convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom; + if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; } } + private void SetVertices(ConvexHull convexHull, Rectangle rect) + { + var verts = GetConvexHullCorners(rect); + Vector2 center = (verts[0] + verts[2]) / 2; + convexHull.SetVertices( + verts, + IsHorizontal ? + new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } : + new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) }); + } + partial void UpdateProjSpecific(float deltaTime) { + convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom; + if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; } if (shakeTimer > 0.0f) { shakeTimer -= deltaTime; @@ -182,7 +194,7 @@ namespace Barotrauma.Items.Components if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured - color *= (item.Condition / item.MaxCondition); + color = color.Multiply(item.Condition / item.MaxCondition); color.A = 255; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index f50239f35..428996d21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -88,6 +88,11 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { + UInt16 userID = msg.ReadUInt16(); + if (userID != Entity.NullEntityID) + { + user = Entity.FindEntityByID(userID) as Character; + } CurrPowerConsumption = powerConsumption; charging = true; timer = Duration; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index fd8f360eb..2f23d59d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -1,13 +1,10 @@ using Barotrauma.Particles; +using Barotrauma.Sounds; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Text; -using System.Xml.Linq; -using Barotrauma.Sounds; using System.Linq; namespace Barotrauma.Items.Components @@ -169,11 +166,11 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { Vector2 particlePos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - float rotation = -item.body.Rotation; + float rotation = item.body.Rotation; if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } foreach (ParticleEmitter emitter in particleEmitters) { - emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: rotation); + emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: -rotation); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 345411caa..ebd078a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -505,13 +505,14 @@ namespace Barotrauma.Items.Components } ActionType type; + string typeStr = subElement.GetAttributeString("type", ""); try { - type = (ActionType)Enum.Parse(typeof(ActionType), subElement.GetAttributeString("type", ""), true); + type = (ActionType)Enum.Parse(typeof(ActionType), typeStr, true); } catch (Exception e) { - DebugConsole.ThrowError("Invalid sound type in " + subElement + "!", e); + DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e); break; } @@ -524,11 +525,13 @@ namespace Barotrauma.Items.Components VolumeProperty = subElement.GetAttributeIdentifier("volumeproperty", "") }; - if (soundSelectionModes == null) soundSelectionModes = new Dictionary(); + if (soundSelectionModes == null) + { + soundSelectionModes = new Dictionary(); + } if (!soundSelectionModes.ContainsKey(type) || soundSelectionModes[type] == SoundSelectionMode.Random) { - Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode); - soundSelectionModes[type] = selectionMode; + soundSelectionModes[type] = subElement.GetAttributeEnum("selectionmode", SoundSelectionMode.Random); } if (!sounds.TryGetValue(itemSound.Type, out List soundList)) @@ -584,6 +587,8 @@ namespace Barotrauma.Items.Components { if (GuiFrame != null && GuiFrameSource.GetAttributeBool("draggable", true)) { + bool hideDragIcons = GuiFrameSource.GetAttributeBool("hidedragicons", false); + var handle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), GuiFrame.RectTransform, style: null) { @@ -623,7 +628,7 @@ namespace Barotrauma.Items.Components }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); - new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, style: "GUIButtonSettings") { OnClicked = (btn, userdata) => @@ -648,6 +653,12 @@ namespace Barotrauma.Items.Components return true; } }; + + if (hideDragIcons) + { + dragIcon.Visible = false; + settingsIcon.Visible = false; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5b16c7d9c..17293bf7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -277,7 +277,8 @@ namespace Barotrauma.Items.Components int ignoredItemCount = 0; var subContainableItems = AllSubContainableItems; - float capacity = GetMaxStackSize(targetSlot); + float targetSlotCapacity = GetMaxStackSize(targetSlot); + float capacity = targetSlotCapacity * MainContainerCapacity; if (subContainableItems != null) { bool useMainContainerCapacity = true; @@ -299,15 +300,11 @@ namespace Barotrauma.Items.Components } if (!useMainContainerCapacity) { break; } } - if (useMainContainerCapacity) - { - capacity *= MainContainerCapacity; - } - else + if (!useMainContainerCapacity) { // Ignore all items in the main container. ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); - capacity *= Capacity - MainContainerCapacity; + capacity = targetSlotCapacity * (Capacity - MainContainerCapacity); } } int itemCount = Inventory.AllItems.Count() - ignoredItemCount; @@ -391,63 +388,60 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (Item containedItem in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(containedItem); - if (relatedItem != null) + + if (contained.Item?.Sprite == null) { continue; } + + if (contained.Hide) { continue; } + if (contained.ItemPos.HasValue) { - if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (item.Submarine != null) - { - itemPos += item.Submarine.DrawPosition; - } - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (item.Submarine != null) + { + itemPos += item.Submarine.DrawPosition; + } + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; } } } - - if (containedItem?.Sprite == null) { continue; } - + if (AutoInteractWithContained) { - containedItem.IsHighlighted = item.IsHighlighted; + contained.Item.IsHighlighted = item.IsHighlighted; item.IsHighlighted = false; } - Vector2 origin = containedItem.Sprite.Origin; - if (item.FlippedX) { origin.X = containedItem.Sprite.SourceRect.Width - origin.X; } - if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } + Vector2 origin = contained.Item.Sprite.Origin; + if (item.FlippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; } + if (item.FlippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } - float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; + float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; if (i < containedSpriteDepths.Length) { containedSpriteDepth = containedSpriteDepths[i]; @@ -456,9 +450,9 @@ namespace Barotrauma.Items.Components SpriteEffects spriteEffects = SpriteEffects.None; float spriteRotation = ItemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - spriteRotation = relatedItem.Rotation; + spriteRotation = contained.Rotation; } if ((item.body != null && item.body.Dir == -1) || item.FlippedX) { @@ -469,17 +463,17 @@ namespace Barotrauma.Items.Components spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } - containedItem.Sprite.Draw( + contained.Item.Sprite.Draw( spriteBatch, new Vector2(itemPos.X, -itemPos.Y), - isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), + isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), - containedItem.Scale, + -(contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), + contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); - foreach (ItemContainer ic in containedItem.GetComponents()) + foreach (ItemContainer ic in contained.Item.GetComponents()) { if (ic.hideItems) { continue; } ic.DrawContainedItems(spriteBatch, containedSpriteDepth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 2c764937c..763d89b4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -14,6 +14,16 @@ namespace Barotrauma.Items.Components private CoroutineHandle resetPredictionCoroutine; private float resetPredictionTimer; + /// + /// The current multiplier for the light color (usually equal to , but in the case of e.g. blinking lights the multiplier + /// doesn't go to 0 when the light turns off, because otherwise it'd take a while for it turn back on based on the lightBrightness which is interpolated + /// towards the current voltage). + /// + private float lightColorMultiplier; + + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the light sprite.")] + public float LightSpriteScale { get; set; } + public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -27,21 +37,14 @@ namespace Barotrauma.Items.Components Light.Position = ParentBody != null ? ParentBody.Position : item.Position; } - partial void SetLightSourceState(bool enabled, float? brightness) + partial void SetLightSourceState(bool enabled, float brightness) { if (Light == null) { return; } Light.Enabled = enabled; - if (brightness.HasValue) - { - lightBrightness = brightness.Value; - } - else - { - lightBrightness = enabled ? 1.0f : 0.0f; - } + lightColorMultiplier = brightness; if (enabled) { - Light.Color = LightColor.Multiply(lightBrightness); + Light.Color = LightColor.Multiply(lightColorMultiplier); } } @@ -92,7 +95,13 @@ namespace Barotrauma.Items.Components { color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); } - Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + Light.LightSprite.Draw(spriteBatch, + new Vector2(drawPos.X, -drawPos.Y), + color * lightBrightness, + origin, + -Light.Rotation, + item.Scale * LightSpriteScale, + Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3debe66f7..1dd6f4341 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -185,6 +185,7 @@ namespace Barotrauma.Items.Components RefreshActivateButtonText(); if (GameMain.Client != null) { + pendingFabricatedItem = null; item.CreateClientEvent(this); } return true; @@ -336,8 +337,11 @@ namespace Barotrauma.Items.Components int calculatePlacement(FabricationRecipe recipe) { + if (recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem)) + { + return -2; + } int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1; - placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0; return placement; } @@ -524,7 +528,7 @@ namespace Barotrauma.Items.Components if (slotRect.Contains(PlayerInput.MousePosition)) { - var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); + var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name).Distinct(); LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) @@ -546,9 +550,11 @@ namespace Barotrauma.Items.Components { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); } + + toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖"; if (!requiredItemPrefab.Description.IsNullOrEmpty()) { - toolTipText += '\n' + requiredItemPrefab.Description; + toolTipText = '\n' + requiredItemPrefab.Description; } tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText }; } @@ -590,7 +596,7 @@ namespace Barotrauma.Items.Components if (tooltip != null) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement); + GUIComponent.DrawToolTip(spriteBatch, RichString.Rich(tooltip.Tooltip), tooltip.TargetElement); tooltip = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index f901ec9ea..969a67312 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Items.Components } } - OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -452,7 +452,7 @@ namespace Barotrauma.Items.Components foreach (var (entity, component) in electricalMapComponents) { GUIComponent parent = component.RectComponent; - if (!(entity is Item it )) { continue; } + if (entity is not Item it ) { continue; } Sprite? sprite = it.Prefab.UpgradePreviewSprite; if (sprite is null) { continue; } @@ -476,7 +476,7 @@ namespace Barotrauma.Items.Components { if (!hullPointsOfInterest.Contains(entity)) { continue; } - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } const int borderMaxSize = 2; if (it.GetComponent() is { }) @@ -643,7 +643,7 @@ namespace Barotrauma.Items.Components elementSize = GuiFrame.Rect.Size; } - float distort = 1.0f - item.Condition / item.MaxCondition; + float distort = item.Repairables.Any(r => r.IsBelowRepairThreshold) ? 1.0f - item.Condition / item.MaxCondition : 0.0f; foreach (HullData hullData in hullDatas.Values) { hullData.DistortionTimer -= deltaTime; @@ -702,6 +702,12 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { + if (miniMapFrame == null) + { + //frame not created yet, could happen if the item hasn't been inside any sub this round? + return; + } + if (Voltage < MinVoltage) { Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); @@ -1130,7 +1136,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) { - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; } if (entity.Removed) @@ -1220,7 +1226,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, component) in hullStatusComponents) { - if (!(entity is Hull hull)) { continue; } + if (entity is not Hull hull) { continue; } if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } if (hullData.Distort) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 628f5a54b..9c03c2a3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -17,6 +17,7 @@ namespace Barotrauma.Items.Components Default, Disruption, Destructible, + Door, LongRange } @@ -110,6 +111,10 @@ namespace Barotrauma.Items.Components BlipType.Destructible, new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } }, + { + BlipType.Door, + new Color[] { Color.TransparentBlack, new Color(73, 78, 86), new Color(66, 94, 100), new Color(47, 115, 58), new Color(255, 255, 255) } + }, { BlipType.LongRange, new Color[] { Color.TransparentBlack, Color.TransparentBlack, new Color(254, 68, 19) * 0.8f, Color.TransparentBlack } @@ -975,7 +980,7 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession == null || Level.Loaded == null) { return; } - if (Level.Loaded.StartLocation != null) + if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, @@ -985,7 +990,7 @@ namespace Barotrauma.Items.Components displayScale, center, DisplayRadius); } - if (Level.Loaded.EndLocation != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection) + if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, @@ -1010,19 +1015,19 @@ namespace Barotrauma.Items.Components int missionIndex = 0; foreach (Mission mission in GameMain.GameSession.Missions) { - if (!mission.SonarLabel.IsNullOrWhiteSpace()) + int i = 0; + foreach ((LocalizedString label, Vector2 position) in mission.SonarLabels) { - int i = 0; - foreach (Vector2 sonarPosition in mission.SonarPositions) + if (!string.IsNullOrEmpty(label.Value)) { DrawMarker(spriteBatch, - mission.SonarLabel.Value, + label.Value, mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, - sonarPosition, transducerCenter, + position, transducerCenter, displayScale, center, DisplayRadius * 0.95f); - i++; } + i++; } missionIndex++; } @@ -1176,13 +1181,18 @@ namespace Barotrauma.Items.Components if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's - if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } + if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && + !dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon) + { + continue; + } //don't show the docking ports of the opposing team on the sonar if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && - dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) + !dockingPort.Item.Submarine.Info.IsOutpost && + !dockingPort.Item.Submarine.Info.IsBeacon) { // specifically checking for friendlyNPC seems more logical here if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } @@ -1348,6 +1358,38 @@ namespace Barotrauma.Items.Components } } + public void RegisterExplosion(Explosion explosion, Vector2 worldPosition) + { + if (Character.Controlled?.SelectedItem != item) { return; } + if (explosion.Attack.StructureDamage <= 0 && explosion.Attack.ItemDamage <= 0 && explosion.EmpStrength <= 0) { return; } + Vector2 transducerCenter = GetTransducerPos(); + if (Vector2.DistanceSquared(worldPosition, transducerCenter) > range * range) { return; } + int blipCount = MathHelper.Clamp((int)(explosion.Attack.Range / 100.0f), 0, 50); + for (int i = 0; i < blipCount; i++) + { + sonarBlips.Add(new SonarBlip( + worldPosition + Rand.Vector(Rand.Range(0.0f, explosion.Attack.Range)), + 1.0f, + Rand.Range(0.5f, 1.0f), + BlipType.Disruption)); + } + if (explosion.EmpStrength > 0.0f) + { + int empBlipCount = MathHelper.Clamp((int)(blipCount * explosion.EmpStrength), 10, 50); + for (int i = 0; i < empBlipCount; i++) + { + Vector2 dir = Rand.Vector(1.0f); + var longRangeBlip = new SonarBlip(worldPosition, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), BlipType.LongRange) + { + Velocity = dir * MathUtils.Round(Rand.Range(4000.0f, 6000.0f), 1000.0f), + Rotation = (float)Math.Atan2(-dir.Y, dir.X) + }; + longRangeBlip.Size.Y *= 4.0f; + sonarBlips.Add(longRangeBlip); + } + } + } + private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive, float pingStrength = 1.0f, AITarget needsToBeInSector = null) { @@ -1392,6 +1434,16 @@ namespace Barotrauma.Items.Components if (connectedSubs.Contains(submarine)) { continue; } } + //display the actual walls if the ping source is inside the sub (but not inside a hull, that's handled above) + //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts + Rectangle worldBorders = submarine.GetDockedBorders(); + worldBorders.Location += submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, pingSource)) + { + CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); + continue; + } + for (int i = 0; i < submarine.HullVertices.Count; i++) { Vector2 start = ConvertUnits.ToDisplayUnits(submarine.HullVertices[i]); @@ -1608,6 +1660,40 @@ namespace Barotrauma.Items.Components } } + private void CreateBlipsForSubmarineWalls(Submarine sub, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float range, bool passive) + { + foreach (Structure structure in Structure.WallList) + { + if (structure.Submarine != sub) { continue; } + CreateBlips(structure.IsHorizontal, structure.WorldPosition, structure.WorldRect); + } + foreach (var door in Door.DoorList) + { + if (door.Item.Submarine != sub || door.IsOpen) { continue; } + CreateBlips(door.IsHorizontal, door.Item.WorldPosition, door.Item.WorldRect, BlipType.Door); + } + + void CreateBlips(bool isHorizontal, Vector2 worldPos, Rectangle worldRect, BlipType blipType = BlipType.Default) + { + Vector2 point1, point2; + if (isHorizontal) + { + point1 = new Vector2(worldRect.X, worldPos.Y); + point2 = new Vector2(worldRect.Right, worldPos.Y); + } + else + { + point1 = new Vector2(worldPos.X, worldRect.Y); + point2 = new Vector2(worldPos.X, worldRect.Y - worldRect.Height); + } + CreateBlipsForLine( + point1, + point2, + pingSource, transducerPos, + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, blipType); + } + } + private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos) { Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 3759e9fd1..6a5d4a44b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -178,8 +178,9 @@ namespace Barotrauma.Items.Components var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); + int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), - TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") + ToolBox.LimitString(TextManager.Get("SteeringMaintainPos"), GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { UserData = UIHighlightAction.ElementId.MaintainPosTickBox, Enabled = autoPilot, @@ -214,7 +215,6 @@ namespace Barotrauma.Items.Components return true; } }; - int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); 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), font: GUIStyle.SmallFont, style: "GUIRadioButton") @@ -340,6 +340,10 @@ namespace Barotrauma.Items.Components centerText = $"({TextManager.Get("Meter")})"; rightTextGetter = () => { + if (Level.Loaded is { IsEndBiome: true }) + { + return Timing.TotalTime % 5.0f < 0.5f ? Rand.Range(-9000, 9000).ToString() : "ERROR"; + } float realWorldDepth = controlledSub == null ? -1000.0f : controlledSub.RealWorldDepth; return ((int)realWorldDepth).ToString(); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 28a25c88e..6a2b6571b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -20,6 +20,7 @@ namespace Barotrauma.Items.Components User = Entity.FindEntityByID(userId) as Character; Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); + SpreadCounter = msg.ReadByte(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index afaeb2dca..c25a7c6cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -262,9 +262,11 @@ namespace Barotrauma.Items.Components } } + float conditionPercentage = item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f; + for (int i = 0; i < particleEmitters.Count; i++) { - if ((item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) + if ((conditionPercentage >= particleEmitterConditionRanges[i].X && conditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) { particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); } @@ -436,12 +438,16 @@ namespace Barotrauma.Items.Components ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; - item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); - if (CurrentFixer == null) + + if (CurrentFixer is null) { qteTimer = QteDuration; qteCooldown = 0.0f; } + else + { + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); + } } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 9526f7f63..5bee8bba0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components { startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } + startPos -= turret.GetRecoilOffset(); } else if (weapon != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 022972368..a83d9a305 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -259,7 +259,7 @@ namespace Barotrauma.Items.Components { bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item); DraggingConnected.RemoveConnection(panel.Item); - if (DraggingConnected.Connect(this, !alreadyConnected, true)) + if (DraggingConnected.TryConnect(this, !alreadyConnected, true)) { var otherConnection = DraggingConnected.OtherConnection(this); ConnectWire(DraggingConnected); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 915e8f695..9f755a3a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -110,8 +110,8 @@ namespace Barotrauma.Items.Components if (HighlightedWire != null) { HighlightedWire.Item.IsHighlighted = true; - if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) HighlightedWire.Connections[0].Item.IsHighlighted = true; - if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) HighlightedWire.Connections[1].Item.IsHighlighted = true; + if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) { HighlightedWire.Connections[0].Item.IsHighlighted = true; } + if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) { HighlightedWire.Connections[1].Item.IsHighlighted = true; } } } @@ -225,7 +225,7 @@ namespace Barotrauma.Items.Components foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray()) { connection.ConnectWire(wire); - wire.Connect(connection, false); + wire.TryConnect(connection, false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 29b3adae2..1668f9739 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components private readonly List uiElements = new List(); private GUILayoutGroup uiElementContainer; + private bool readingNetworkEvent; + private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); public override bool RecreateGUIOnResolutionChange => true; @@ -100,7 +102,7 @@ namespace Barotrauma.Items.Components { ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -126,7 +128,7 @@ namespace Barotrauma.Items.Components { ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -161,7 +163,7 @@ namespace Barotrauma.Items.Components { TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -181,12 +183,12 @@ namespace Barotrauma.Items.Components }; btn.OnClicked += (_, userdata) => { - CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;; + CustomInterfaceElement btnElement = userdata as CustomInterfaceElement; if (GameMain.Client == null) { ButtonClicked(btnElement); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this, new EventData(btnElement)); } @@ -297,9 +299,10 @@ namespace Barotrauma.Items.Components LocalizedString CreateLabelText(int elementIndex) { - return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? + var label = customInterfaceElementList[elementIndex].Label; + return string.IsNullOrWhiteSpace(label) ? TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : - customInterfaceElementList[elementIndex].Label; + TextManager.Get(label).Fallback(label); } uiElementContainer.Recalculate(); @@ -386,45 +389,53 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { - for (int i = 0; i < customInterfaceElementList.Count; i++) + readingNetworkEvent = true; + try { - var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + for (int i = 0; i < customInterfaceElementList.Count; i++) { - string newValue = msg.ReadString(); - if (!element.IsNumberInput) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - TextChanged(element, newValue); + string newValue = msg.ReadString(); + if (!element.IsNumberInput) + { + TextChanged(element, newValue); + } + else + { + switch (element.NumberType) + { + case NumberType.Int when int.TryParse(newValue, out int value): + ValueChanged(element, value); + break; + case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): + ValueChanged(element, value); + break; + } + } } else { - switch (element.NumberType) + bool elementState = msg.ReadBoolean(); + if (element.ContinuousSignal) { - case NumberType.Int when int.TryParse(newValue, out int value): - ValueChanged(element, value); - break; - case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): - ValueChanged(element, value); - break; + ((GUITickBox)uiElements[i]).Selected = elementState; + TickBoxToggled(element, elementState); + } + else if (elementState) + { + ButtonClicked(element); } } } - else - { - bool elementState = msg.ReadBoolean(); - if (element.ContinuousSignal) - { - ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(element, elementState); - } - else if (elementState) - { - ButtonClicked(element); - } - } - } - UpdateSignalsProjSpecific(); + UpdateSignalsProjSpecific(); + } + finally + { + readingNetworkEvent = false; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index ec3054df9..c879cb159 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; using System.Xml.Linq; @@ -24,7 +25,9 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { - var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }) + float marginMultiplier = element.GetAttributeFloat("marginmultiplier", 1.0f); + + var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) }) { ChildAnchor = Anchor.TopCenter, RelativeSpacing = 0.02f, @@ -33,31 +36,34 @@ namespace Barotrauma.Items.Components historyBox = new GUIListBox(new RectTransform(new Vector2(1, .9f), layoutGroup.RectTransform), style: null) { - AutoHideScrollBar = false + AutoHideScrollBar = this.AutoHideScrollbar }; - CreateFillerBlock(); - - new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); - - inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) + if (!Readonly) { - MaxTextLength = MaxMessageLength, - OverflowClip = true, - OnEnterPressed = (GUITextBox textBox, string text) => + CreateFillerBlock(); + + new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); + + inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) { - if (GameMain.NetworkMember == null) + MaxTextLength = MaxMessageLength, + OverflowClip = true, + OnEnterPressed = (GUITextBox textBox, string text) => { - SendOutput(text); + if (GameMain.NetworkMember == null) + { + SendOutput(text); + } + else + { + item.CreateClientEvent(this, new ClientEventData(text)); + } + textBox.Text = string.Empty; + return true; } - else - { - item.CreateClientEvent(this, new ClientEventData(text)); - } - textBox.Text = string.Empty; - return true; - } - }; + }; + } layoutGroup.Recalculate(); } @@ -101,7 +107,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), - "> " + input, + LineStartSymbol + TextManager.Get(input).Fallback(input), textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.Font) { CanBeFocused = false @@ -123,7 +129,10 @@ namespace Barotrauma.Items.Components historyBox.RecalculateChildren(); historyBox.UpdateScrollBarSize(); - historyBox.ScrollBar.BarScrollValue = 1; + if (AutoScrollToBottom) + { + historyBox.ScrollBar.BarScrollValue = 1; + } } public override bool Select(Character character) @@ -138,7 +147,7 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList(int order = 0) { base.AddToGUIUpdateList(order: order); - if (shouldSelectInputBox) + if (shouldSelectInputBox && !Readonly) { inputBox.Select(); shouldSelectInputBox = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index afe938020..426d2c127 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; foreach (Limb limb in c.AnimController.Limbs) { - if (limb.Mass < 1.0f) { continue; } + if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; } float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f); float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f); Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); @@ -302,7 +302,18 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) { continue; } + if (affliction.Strength <= 0f) { continue; } + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold) + { + if (target.IsHuman || target.IsOnPlayerTeam || (affliction.Prefab.AfflictionType != AfflictionPrefab.PoisonType && affliction.Prefab.AfflictionType != AfflictionPrefab.ParalysisType)) + { + // Always show the poisons on monsters, because poisoning bigger monsters require multiple doses. + // The solution is hacky, but didn't want to introduce an extra property for this. + // We also want to have a relatively high thershold for showing the poisons on the scanner on humans, so that it's not instantly clear that a target is poisoned and especially not which poison was used. + // Paralysis is treated like a poison but isn't technically a poison, so that we can have multiple afflictions that still are treated the same. + continue; + } + } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index e66a15de4..402805f70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -333,15 +333,9 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } - - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) - { - if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) - { - UpdateTransformedBarrelPos(); - } - Vector2 drawPos = GetDrawPos(); + public Vector2 GetRecoilOffset() + { float recoilOffset = 0.0f; if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { @@ -362,6 +356,17 @@ namespace Barotrauma.Items.Components recoilOffset = RecoilDistance; } } + return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; + } + + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + { + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) + { + UpdateTransformedBarrelPos(); + } + Vector2 drawPos = GetDrawPos(); + railSprite?.Draw(spriteBatch, drawPos, @@ -370,7 +375,7 @@ namespace Barotrauma.Items.Components SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); barrelSprite?.Draw(spriteBatch, - drawPos - new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset * item.Scale, + drawPos - GetRecoilOffset() * item.Scale, item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 5b4d60065..96607abbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -53,11 +53,11 @@ namespace Barotrauma get { // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen - if (IsMoving) return offScreenRect; + if (IsMoving) { return offScreenRect; } int buttonDir = Math.Sign(SubInventoryDir); - float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale * Inventory.IndicatorScaleAdjustment; + float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale; Vector2 equipIndicatorPos = new Vector2(Rect.Left, Rect.Center.Y + (Rect.Height / 2 + 15 * Inventory.UIScale) * buttonDir - sizeY / 2f); equipIndicatorPos += DrawOffset; @@ -176,14 +176,6 @@ namespace Barotrauma public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; - public static float IndicatorScaleAdjustment - { - get - { - return !GUI.IsFourByThree() ? 0.8f : 0.7f; - } - } - public static Inventory DraggingInventory; public Inventory ReplacedBy; @@ -249,11 +241,11 @@ namespace Barotrauma { itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); } - Tooltip = GetTooltip(Item, itemsInSlot); + Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled); tooltipDisplayedCondition = (int)Item.ConditionPercentage; } - private RichString GetTooltip(Item item, IEnumerable itemsInSlot) + private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, Character character) { if (item == null) { return null; } @@ -348,10 +340,12 @@ namespace Barotrauma } if (itemsInSlot.Count() > 1) { - string colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; - colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + } + if (item.Prefab.SkillRequirementHints != null && item.Prefab.SkillRequirementHints.Any()) + { + toolTip += item.Prefab.GetSkillRequirementHints(character); } return RichString.Rich(toolTip); } @@ -576,7 +570,7 @@ namespace Barotrauma } } - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { mouseOn = false; } @@ -727,14 +721,7 @@ namespace Barotrauma Rectangle subRect = slot.Rect; Vector2 spacing; - if (GUI.IsFourByThree()) - { - spacing = new Vector2(5 * UIScale, (5 + UnequippedIndicator.size.Y) * UIScale); - } - else - { - spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale); - } + spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale * GUI.AspectRatioAdjustment); int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow); while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f) @@ -1535,16 +1522,6 @@ namespace Barotrauma { Sprite slotSprite = slot.SlotSprite ?? SlotSpriteSmall; - /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null - && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) - { - slotColor = slot.IsHighlighted ? GUIStyle.EquipmentSlotColor : GUIStyle.EquipmentSlotColor * 0.8f; - } - else - { - slotColor = slot.IsHighlighted ? GUIStyle.InventorySlotColor : GUIStyle.InventorySlotColor * 0.8f; - }*/ - if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor); @@ -1731,7 +1708,17 @@ namespace Barotrauma slot.InventoryKeyIndex < GameSettings.CurrentConfig.InventoryKeyMap.Bindings.Length) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); + + GUIStyle.HotkeyFont.DrawString( + spriteBatch, + GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, + rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), + Color.Black, + rotation: 0.0f, + origin: Vector2.Zero, + scale: Vector2.One * GUI.AspectRatioAdjustment, + SpriteEffects.None, + layerDepth: 0.0f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 684cb0d5c..0f85d846d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -23,7 +23,21 @@ namespace Barotrauma private readonly List activeEditors = new List(); - public GUIComponentStyle IconStyle { get; private set; } = null; + + private GUIComponentStyle iconStyle; + public GUIComponentStyle IconStyle + { + get { return iconStyle; } + private set + { + if (IconStyle != value) + { + iconStyle = value; + CheckIsHighlighted(); + } + } + } + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) { if (interactionType == CampaignMode.InteractionType.None) @@ -143,6 +157,18 @@ namespace Barotrauma return color; } + protected override void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight || IconStyle != null) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + public Color GetInventoryIconColor() { Color color = InventoryIconColor; @@ -281,7 +307,8 @@ namespace Barotrauma cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint()); } - Vector2 worldPosition = WorldPosition; + Vector2 worldPosition = WorldPosition + GetCollapseEffectOffset(); + if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; } if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; } @@ -310,7 +337,9 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; - Vector2 drawOffset = Vector2.Zero; + Vector2 drawOffset = GetCollapseEffectOffset(); + drawOffset.Y = -drawOffset.Y; + if (displayCondition < MaxCondition) { for (int i = 0; i < Prefab.BrokenSprites.Length; i++) @@ -426,6 +455,8 @@ namespace Barotrauma var holdable = GetComponent(); if (holdable != null && holdable.Picker?.AnimController != null) { + //don't draw the item on hands if it's also being worn + if (GetComponent() is { IsActive: true }) { return; } if (!back) { return; } float depthStep = 0.000001f; if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) @@ -466,12 +497,12 @@ namespace Barotrauma 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 = (float)Math.Cos(-body.Rotation); - var sa = (float)Math.Sin(-body.Rotation); + 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(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, + 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)); } } @@ -728,7 +759,7 @@ namespace Barotrauma if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - var otherEntity = mapEntityList.FirstOrDefault(e => e != this && e.IsHighlighted && e.IsMouseOn(position)); + var otherEntity = highlightedEntities.FirstOrDefault(e => e != this && e.IsMouseOn(position)); if (otherEntity != null) { if (linkedTo.Contains(otherEntity)) @@ -1385,8 +1416,9 @@ namespace Barotrauma } break; case EventType.Status: + bool loadingRound = msg.ReadBoolean(); float newCondition = msg.ReadSingle(); - SetCondition(newCondition, isNetworkEvent: true); + SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound); break; case EventType.AssignCampaignInteraction: CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); @@ -1672,25 +1704,24 @@ namespace Barotrauma bool hasIdCard = msg.ReadBoolean(); string ownerName = "", ownerTags = ""; int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1; - Color ownerHairColor = Microsoft.Xna.Framework.Color.White, - ownerFacialHairColor = Microsoft.Xna.Framework.Color.White, - ownerSkinColor = Microsoft.Xna.Framework.Color.White; + Color ownerHairColor = Color.White, + ownerFacialHairColor = Color.White, + ownerSkinColor = Color.White; Identifier ownerJobId = Identifier.Empty; Vector2 ownerSheetIndex = Vector2.Zero; + int submarineSpecificId = 0; if (hasIdCard) { + submarineSpecificId = msg.ReadInt32(); ownerName = msg.ReadString(); - ownerTags = msg.ReadString(); - + ownerTags = msg.ReadString(); ownerBeardIndex = msg.ReadByte() - 1; ownerHairIndex = msg.ReadByte() - 1; ownerMoustacheIndex = msg.ReadByte() - 1; - ownerFaceAttachmentIndex = msg.ReadByte() - 1; - + ownerFaceAttachmentIndex = msg.ReadByte() - 1; ownerHairColor = msg.ReadColorR8G8B8(); ownerFacialHairColor = msg.ReadColorR8G8B8(); - ownerSkinColor = msg.ReadColorR8G8B8(); - + ownerSkinColor = msg.ReadColorR8G8B8(); ownerJobId = msg.ReadIdentifier(); int x = msg.ReadByte(); @@ -1794,6 +1825,7 @@ namespace Barotrauma } foreach (IdCard idCard in item.GetComponents()) { + idCard.SubmarineSpecificID = submarineSpecificId; idCard.TeamID = (CharacterTeamType)teamID; idCard.OwnerName = ownerName; idCard.OwnerTags = ownerTags; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 17cd3e848..49c3eb0e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -254,7 +254,7 @@ namespace Barotrauma if (!DefaultPrice.RequiresUnlock) { return true; } return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); } - public LocalizedString GetTooltip() + public LocalizedString GetTooltip(Character character) { LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; if (!Description.IsNullOrEmpty()) @@ -265,6 +265,10 @@ namespace Barotrauma { Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); } + if (SkillRequirementHints != null && SkillRequirementHints.Any()) + { + tooltip += GetSkillRequirementHints(character); + } return tooltip; } @@ -376,5 +380,31 @@ namespace Barotrauma Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } + + public LocalizedString GetSkillRequirementHints(Character character) + { + LocalizedString text = ""; + if (SkillRequirementHints != null && SkillRequirementHints.Any() && character != null) + { + Color orange = GUIStyle.Orange; + // Reuse an existing, localized, text because it's what we want here: "Required skills:" + text = "\n\n" + $"‖color:{orange.ToStringHex()}‖{TextManager.Get("requiredrepairskills")}‖color:end‖"; + foreach (var hint in SkillRequirementHints) + { + int skillLevel = (int)character.GetSkillLevel(hint.Skill); + Color levelColor = GUIStyle.Yellow; + if (skillLevel >= hint.Level) + { + levelColor = GUIStyle.Green; + } + else if (skillLevel < hint.Level / 2) + { + levelColor = GUIStyle.Red; + } + text += "\n" + hint.GetFormattedText(skillLevel, levelColor.ToStringHex()); + } + } + return text; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 77189d2d9..362dafa31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -320,5 +321,45 @@ namespace Barotrauma return IsHorizontal ? rect.Height : rect.Width; } } + + public override void UpdateEditing(Camera cam, float deltaTime) + { + if (editingHUD == null || editingHUD.UserData != this) + { + editingHUD = CreateEditingHUD(); + } + } + private GUIComponent CreateEditingHUD(bool inGame = false) + { + + editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) + { + UserData = this + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = (int)(GUI.Scale * 5) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("entityname.gap"), font: GUIStyle.LargeFont); + var hiddenInGameTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), TextManager.Get("sp.hiddeningame.name")) + { + Selected = HiddenInGame + }; + hiddenInGameTickBox.OnSelected += (GUITickBox tickbox) => + { + HiddenInGame = tickbox.Selected; + return true; + }; + editingHUD.RectTransform.Resize(new Point( + editingHUD.Rect.Width, + (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y * 1.25f))); + + PositionEditingHUD(); + + return editingHUD; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 095195834..5eb9bcd95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -129,20 +129,15 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted) { continue; } + if (entity == this) { continue; } if (!entity.IsMouseOn(position)) { continue; } if (entity.linkedTo == null || !entity.Linkable) { continue; } if (entity.linkedTo.Contains(this) || linkedTo.Contains(entity) || rClick) { - if (entity == this || !entity.IsHighlighted) { continue; } - if (!entity.IsMouseOn(position)) { continue; } - if (entity.linkedTo.Contains(this)) - { - entity.linkedTo.Remove(this); - linkedTo.Remove(entity); - } + entity.linkedTo.Remove(this); + linkedTo.Remove(entity); } else { @@ -329,13 +324,13 @@ namespace Barotrauma } - /*GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); + GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); for (int i = 0; i < waveY.Length - 1; i++) { GUI.DrawLine(spriteBatch, new Vector2(drawRect.X + WaveWidth * i, -WorldSurface - waveY[i] - 10), new Vector2(drawRect.X + WaveWidth * (i + 1), -WorldSurface - waveY[i + 1] - 10), Color.Blue * 0.5f); - }*/ + } } foreach (MapEntity e in linkedTo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 67a6e6fa2..38299827b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -114,7 +114,11 @@ namespace Barotrauma graphics.Clear(BackgroundColor); - renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + if (renderer != null) + { + GameMain.LightManager.AmbientLight = GameMain.LightManager.AmbientLight.Add(renderer.FlashColor); + renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + } } public void DrawFront(SpriteBatch spriteBatch, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index f5fc36a94..745af094f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -22,7 +22,7 @@ namespace Barotrauma public float CurrentRotation; - private List spriteDeformations = new List(); + private readonly List spriteDeformations = new List(); public Vector2 CurrentScale { @@ -86,6 +86,8 @@ namespace Barotrauma private set; } + public bool CanBeVisible { get; private set; } + partial void InitProjSpecific() { Sprite?.EnsureLazyLoaded(); @@ -156,6 +158,11 @@ namespace Barotrauma { SonarRadius = Triggers.Select(t => t.ColliderRadius * 1.5f).Max(); } + + CanBeVisible = + Sprite != null || + Prefab.DeformableSprite != null || + Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null)); } public void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index c123c22f0..d23d0b542 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -56,63 +56,84 @@ namespace Barotrauma float minSizeToDraw = MathHelper.Lerp(10.0f, 5.0f, Math.Min(zoom * 20.0f, 1.0f)); + //start from the grid cell at the center of the view + //(if objects needs to be culled, better to cull at the edges of the view) + int midIndexX = (currentIndices.X + currentIndices.Width) / 2; + int midIndexY = (currentIndices.Y + currentIndices.Height) / 2; + CheckIndex(midIndexX, midIndexY); + for (int x = currentIndices.X; x <= currentIndices.Width; x++) { for (int y = currentIndices.Y; y <= currentIndices.Height; y++) { - if (objectGrid[x, y] == null) { continue; } - foreach (LevelObject obj in objectGrid[x, y]) + if (x != midIndexX || y != midIndexY) { CheckIndex(x, y); } + } + } + + void CheckIndex(int x, int y) + { + if (objectGrid[x, y] == null) { return; } + foreach (LevelObject obj in objectGrid[x, y]) + { + if (!obj.CanBeVisible) { continue; } + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } + + if (zoom < 0.05f) { - if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - - if (zoom < 0.05f) + //hide if the sprite is very small when zoomed this far out + if ((obj.Sprite != null && Math.Min(obj.Sprite.size.X * zoom, obj.Sprite.size.Y * zoom) < 5.0f) || + (obj.ActivePrefab?.DeformableSprite != null && Math.Min(obj.ActivePrefab.DeformableSprite.Sprite.size.X * zoom, obj.ActivePrefab.DeformableSprite.Sprite.size.Y * zoom) < minSizeToDraw)) { - //hide if the sprite is very small when zoomed this far out - if ((obj.Sprite != null && Math.Min(obj.Sprite.size.X * zoom, obj.Sprite.size.Y * zoom) < 5.0f) || - (obj.ActivePrefab?.DeformableSprite != null && Math.Min(obj.ActivePrefab.DeformableSprite.Sprite.size.X * zoom, obj.ActivePrefab.DeformableSprite.Sprite.size.Y * zoom) < minSizeToDraw)) - { - continue; - } - - float zCutoff = MathHelper.Lerp(5000.0f, 500.0f, (0.05f - zoom) * 20.0f); - if (obj.Position.Z > zCutoff) - { - continue; - } + continue; } - var objectList = - obj.Position.Z >= 0 ? - visibleObjectsBack : - (obj.Position.Z < -1 ? visibleObjectsFront : visibleObjectsMid); - int drawOrderIndex = 0; - for (int i = 0; i < objectList.Count; i++) + float zCutoff = MathHelper.Lerp(5000.0f, 500.0f, (0.05f - zoom) * 20.0f); + if (obj.Position.Z > zCutoff) { - if (objectList[i] == obj) - { - drawOrderIndex = -1; - break; - } + continue; + } + } - if (objectList[i].Position.Z < obj.Position.Z) - { - break; - } - else - { - drawOrderIndex = i + 1; - } + var objectList = + obj.Position.Z >= 0 ? + visibleObjectsBack : + (obj.Position.Z < -1 ? visibleObjectsFront : visibleObjectsMid); + if (objectList.Count >= MaxVisibleObjects) { continue; } + + int drawOrderIndex = 0; + for (int i = 0; i < objectList.Count; i++) + { + if (objectList[i] == obj) + { + drawOrderIndex = -1; + break; } - if (drawOrderIndex >= 0) + if (objectList[i].Position.Z > obj.Position.Z) { - objectList.Insert(drawOrderIndex, obj); - if (objectList.Count >= MaxVisibleObjects) { break; } + break; } + else + { + drawOrderIndex = i + 1; + if (drawOrderIndex >= MaxVisibleObjects) { break; } + } + } + + if (drawOrderIndex >= 0 && drawOrderIndex < MaxVisibleObjects) + { + objectList.Insert(drawOrderIndex, obj); } } } + //object grid is sorted in an ascending order + //(so we prefer the objects in the foreground instead of ones in the background if some need to be culled) + //rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists + visibleObjectsBack.Reverse(); + visibleObjectsMid.Reverse(); + visibleObjectsFront.Reverse(); + currentGridIndices = currentIndices; } @@ -144,14 +165,14 @@ namespace Barotrauma { Rectangle indices = Rectangle.Empty; indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize); - if (indices.X >= objectGrid.GetLength(0)) return; + if (indices.X >= objectGrid.GetLength(0)) { return; } indices.Y = (int)Math.Floor((cam.WorldView.Y - cam.WorldView.Height - Level.Loaded.BottomPos) / (float)GridSize); - if (indices.Y >= objectGrid.GetLength(1)) return; + if (indices.Y >= objectGrid.GetLength(1)) { return; } indices.Width = (int)Math.Floor(cam.WorldView.Right / (float)GridSize) + 1; - if (indices.Width < 0) return; + if (indices.Width < 0) { return; } indices.Height = (int)Math.Floor((cam.WorldView.Y - Level.Loaded.BottomPos) / (float)GridSize) + 1; - if (indices.Height < 0) return; + if (indices.Height < 0) { return; } indices.X = Math.Max(indices.X, 0); indices.Y = Math.Max(indices.Y, 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index f92c7a30f..9366776a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -1,4 +1,4 @@ -using FarseerPhysics; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -71,9 +71,12 @@ namespace Barotrauma { private static BasicEffect wallEdgeEffect, wallCenterEffect; - private Vector2 dustOffset; - private Vector2 defaultDustVelocity; - private Vector2 dustVelocity; + private Vector2 waterParticleOffset; + private Vector2 waterParticleVelocity; + + private float flashCooldown; + private float flashTimer; + public Color FlashColor { get; private set; } private readonly RasterizerState cullNone; @@ -81,10 +84,26 @@ namespace Barotrauma private readonly List vertexBuffers = new List(); + private float chromaticAberrationStrength; + public float ChromaticAberrationStrength + { + get { return chromaticAberrationStrength; } + set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + } + public float CollapseEffectStrength + { + get; + set; + } + public Vector2 CollapseEffectOrigin + { + get; + set; + } + + public LevelRenderer(Level level) { - defaultDustVelocity = Vector2.UnitY * 10.0f; - cullNone = new RasterizerState() { CullMode = CullMode.None }; if (wallEdgeEffect == null) @@ -120,12 +139,50 @@ namespace Barotrauma level.GenerationParams.WallSprite.ReloadTexture(); wallCenterEffect.Texture = level.GenerationParams.WallSprite.Texture; } + + public void Flash() + { + flashTimer = 1.0f; + } public void Update(float deltaTime, Camera cam) { + if (CollapseEffectStrength > 0.0f) + { + CollapseEffectStrength = Math.Max(0.0f, CollapseEffectStrength - deltaTime); + } + if (ChromaticAberrationStrength > 0.0f) + { + ChromaticAberrationStrength = Math.Max(0.0f, ChromaticAberrationStrength - deltaTime * 10.0f); + } + + if (level.GenerationParams.FlashInterval.Y > 0) + { + flashCooldown -= deltaTime; + if (flashCooldown <= 0.0f) + { + flashTimer = 1.0f; + if (level.GenerationParams.FlashSound != null) + { + level.GenerationParams.FlashSound.Play(1.0f, "default"); + } + flashCooldown = Rand.Range(level.GenerationParams.FlashInterval.X, level.GenerationParams.FlashInterval.Y, Rand.RandSync.Unsynced); + } + if (flashTimer > 0.0f) + { + float brightness = flashTimer * 1.1f - PerlinNoise.GetPerlin((float)Timing.TotalTime, (float)Timing.TotalTime * 0.66f) * 0.1f; + FlashColor = level.GenerationParams.FlashColor.Multiply(MathHelper.Clamp(brightness, 0.0f, 1.0f)); + flashTimer -= deltaTime * 0.5f; + } + else + { + FlashColor = Color.TransparentBlack; + } + } + //calculate the sum of the forces of nearby level triggers - //and use it to move the dust texture and water distortion effect - Vector2 currentDustVel = defaultDustVelocity; + //and use it to move the water texture and water distortion effect + Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity; foreach (LevelObject levelObject in level.LevelObjectManager.GetVisibleObjects()) { if (levelObject.Triggers == null) { continue; } @@ -139,21 +196,21 @@ namespace Barotrauma objectMaxFlow = vel; } } - currentDustVel += objectMaxFlow; + currentWaterParticleVel += objectMaxFlow; } + + waterParticleVelocity = Vector2.Lerp(waterParticleVelocity, currentWaterParticleVel, deltaTime); - dustVelocity = Vector2.Lerp(dustVelocity, currentDustVel, deltaTime); - - WaterRenderer.Instance?.ScrollWater(dustVelocity, deltaTime); + WaterRenderer.Instance?.ScrollWater(waterParticleVelocity, deltaTime); if (level.GenerationParams.WaterParticles != null) { Vector2 waterTextureSize = level.GenerationParams.WaterParticles.size * level.GenerationParams.WaterParticleScale; - dustOffset += new Vector2(dustVelocity.X, -dustVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; - while (dustOffset.X <= -waterTextureSize.X) dustOffset.X += waterTextureSize.X; - while (dustOffset.X >= waterTextureSize.X) dustOffset.X -= waterTextureSize.X; - while (dustOffset.Y <= -waterTextureSize.Y) dustOffset.Y += waterTextureSize.Y; - while (dustOffset.Y >= waterTextureSize.Y) dustOffset.Y -= waterTextureSize.Y; + waterParticleOffset += new Vector2(waterParticleVelocity.X, -waterParticleVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; + while (waterParticleOffset.X <= -waterTextureSize.X) { waterParticleOffset.X += waterTextureSize.X; } + while (waterParticleOffset.X >= waterTextureSize.X){ waterParticleOffset.X -= waterTextureSize.X; } + while (waterParticleOffset.Y <= -waterTextureSize.Y) { waterParticleOffset.Y += waterTextureSize.Y; } + while (waterParticleOffset.Y >= waterTextureSize.Y) { waterParticleOffset.Y -= waterTextureSize.Y; } } } @@ -234,7 +291,7 @@ namespace Barotrauma Rectangle srcRect = new Rectangle(0, 0, 2048, 2048); Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y); - Vector2 offset = -origin + dustOffset; + Vector2 offset = -origin + waterParticleOffset; while (offset.X <= -srcRect.Width * textureScale) offset.X += srcRect.Width * textureScale; while (offset.X > 0.0f) offset.X -= srcRect.Width * textureScale; while (offset.Y <= -srcRect.Height * textureScale) offset.Y += srcRect.Height * textureScale; @@ -261,7 +318,7 @@ namespace Barotrauma level.GenerationParams.WaterParticles.DrawTiled( spriteBatch, origin + offsetS, new Vector2(cam.WorldView.Width - offsetS.X, cam.WorldView.Height - offsetS.Y), - color: Color.White * alpha, textureScale: new Vector2(texScale)); + color: level.GenerationParams.WaterParticleColor * alpha, textureScale: new Vector2(texScale)); } } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 27b1aaf28..3be5faa17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -1,8 +1,6 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using SharpFont; using System; using System.Collections.Generic; using System.Diagnostics; @@ -12,28 +10,14 @@ namespace Barotrauma.Lights { class ConvexHullList { - private List list; - public HashSet IsHidden; public readonly Submarine Submarine; - public List List - { - get { return list; } - set - { - Debug.Assert(value != null); - Debug.Assert(!list.Contains(null)); - list = value; - IsHidden.RemoveWhere(ch => !list.Contains(ch)); - } - } - + public HashSet IsHidden = new HashSet(); + public readonly List List = new List(); public ConvexHullList(Submarine submarine) { Submarine = submarine; - list = new List(); - IsHidden = new HashSet(); } } @@ -47,13 +31,13 @@ namespace Barotrauma.Lights public bool IsHorizontal; public bool IsAxisAligned; + public Vector2 SubmarineDrawPos; + public Segment(SegmentPoint start, SegmentPoint end, ConvexHull convexHull) { if (start.Pos.Y > end.Pos.Y) { - var temp = start; - start = end; - end = temp; + (end, start) = (start, end); } Start = start; @@ -102,14 +86,15 @@ namespace Barotrauma.Lights private readonly Segment[] segments = new Segment[4]; private readonly SegmentPoint[] vertices = new SegmentPoint[4]; - private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; - private readonly VectorPair[] losOffsets = new VectorPair[4]; - - private readonly bool[] backFacing; - private readonly bool[] ignoreEdge; + private readonly SegmentPoint[] losVertices = new SegmentPoint[2]; + private readonly Vector2[] losOffsets = new Vector2[2]; private readonly bool isHorizontal; + private readonly int thickness; + + public bool IsExteriorWall; + public VertexPositionColor[] ShadowVertices { get; private set; } public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } @@ -145,47 +130,27 @@ namespace Barotrauma.Lights public Rectangle BoundingBox { get; private set; } - public ConvexHull(Vector2[] points, Color color, MapEntity parent) + public ConvexHull(Rectangle rect, bool? isHorizontal, MapEntity parent) { - if (shadowEffect == null) - { - shadowEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true }; - } - if (penumbraEffect == null) - { - penumbraEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + penumbraEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { TextureEnabled = true, LightingEnabled = false, Texture = TextureLoader.FromFile("Content/Lights/penumbra.png") }; - } ParentEntity = parent; ShadowVertices = new VertexPositionColor[6 * 4]; PenumbraVertices = new VertexPositionTexture[6 * 4]; - backFacing = new bool[4]; - ignoreEdge = new bool[4]; + BoundingBox = rect; - float minX = points[0].X, minY = points[0].Y, maxX = points[0].X, maxY = points[0].Y; - - for (int i = 1; i < vertices.Length; i++) - { - if (points[i].X < minX) minX = points[i].X; - if (points[i].Y < minY) minY = points[i].Y; - - if (points[i].X > maxX) maxX = points[i].X; - if (points[i].Y > minY) maxY = points[i].Y; - } - - BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); - - isHorizontal = BoundingBox.Width > BoundingBox.Height; + this.isHorizontal = isHorizontal ?? BoundingBox.Width > BoundingBox.Height; if (ParentEntity is Structure structure) { System.Diagnostics.Debug.Assert(!structure.Removed); @@ -198,8 +163,26 @@ namespace Barotrauma.Lights if (door != null) { isHorizontal = door.IsHorizontal; } } - SetVertices(points); - + Vector2[] verts = new Vector2[] + { + new Vector2(rect.X, rect.Bottom), + new Vector2(rect.Right, rect.Bottom), + new Vector2(rect.Right, rect.Y), + new Vector2(rect.X, rect.Y), + }; + + Vector2[] losVerts; + if (this.isHorizontal) + { + thickness = rect.Height; + losVerts = new Vector2[] { new Vector2(rect.X, rect.Center.Y), new Vector2(rect.Right, rect.Center.Y) }; + } + else + { + thickness = rect.Width; + losVerts = new Vector2[] { new Vector2(rect.Center.X, rect.Y), new Vector2(rect.Center.X, rect.Bottom) }; + } + SetVertices(verts, losVerts); Enabled = true; var chList = HullLists.Find(h => h.Submarine == parent.Submarine); @@ -211,257 +194,72 @@ namespace Barotrauma.Lights foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); - ch.MergeOverlappingSegments(this); + MergeLosVertices(ch); + ch.MergeLosVertices(this); } chList.List.Add(this); } - private void MergeOverlappingSegments(ConvexHull ch) + private void MergeLosVertices(ConvexHull ch, bool refreshOtherOverlappingHulls = true) { if (ch == this) { return; } - if (isHorizontal == ch.isHorizontal) + //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) + float mergeDist = MathHelper.Clamp(ch.thickness * 0.55f, 16, 512); + mergeDist = Math.Min(mergeDist, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) / 2); + + float mergeDistSqr = mergeDist * mergeDist; + + bool changed = false; + for (int i = 0; i < losVertices.Length; i++) { - //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) - float mergeDist = 16; - float mergeDistSqr = mergeDist * mergeDist; + //find the closest point on the other convex hull segment + Vector2 closest = MathUtils.GetClosestPointOnLineSegment( + ch.losVertices[0].Pos + ch.losOffsets[0], + ch.losVertices[1].Pos + ch.losOffsets[1], + losVertices[i].Pos); + if (Vector2.DistanceSquared(closest, losVertices[i].Pos) > mergeDistSqr) { continue; } - Rectangle intersection = Rectangle.Intersect(BoundingBox, ch.BoundingBox); - int intersectionArea = intersection.Width * intersection.Height; - int bboxArea = BoundingBox.Width * BoundingBox.Height; - int otherBboxArea = ch.BoundingBox.Width * ch.BoundingBox.Height; - if (Math.Abs(intersectionArea - bboxArea) < mergeDistSqr) { return; } - if (Math.Abs(intersectionArea - otherBboxArea) < mergeDistSqr) { return; } - - for (int i = 0; i < segments.Length; i++) + //find where the segments would intersect if they had infinite length + // if it's close to the closest point, let's use that instead to keep + // the direction of the segment unchanged (i.e. vertical segment stays vertical) + if (MathUtils.GetLineIntersection( + ch.losVertices[0].Pos + ch.losOffsets[0], + ch.losVertices[1].Pos + ch.losOffsets[1], + losVertices[0].Pos, + losVertices[1].Pos, + out Vector2 intersection)) { - for (int j = 0; j < ch.segments.Length; j++) + if (Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistSqr || + Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f) { - if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; } - if (ignoreEdge[i] || ch.ignoreEdge[j]) { continue; } - - //the segments must be at different sides of the convex hulls to be merged - //(e.g. the right edge of a wall piece and the left edge of another one) - var segment1Center = (segments[i].Start.Pos + segments[i].End.Pos) / 2.0f; - var segment2Center = (ch.segments[j].Start.Pos + ch.segments[j].End.Pos) / 2.0f; - if (Vector2.Dot(segment1Center - BoundingBox.Center.ToVector2(), segment2Center - ch.BoundingBox.Center.ToVector2()) > 0) { continue; } - - if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < mergeDistSqr && - Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < mergeDistSqr) - { - ignoreEdge[i] = true; - ch.ignoreEdge[j] = true; - MergeSegments(segments[i], ch.segments[j], true); - } - else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < mergeDistSqr && - Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < mergeDistSqr) - { - ignoreEdge[i] = true; - ch.ignoreEdge[j] = true; - MergeSegments(segments[i], ch.segments[j], false); - } - } - } - } - - for (int i = 0; i < segments.Length; i++) - { - if (ignoreEdge[i]) { continue; } - if (Vector2.DistanceSquared(segments[i].Start.Pos, segments[i].End.Pos) < 1.0f) { continue; } - for (int j = 0; j < ch.segments.Length; j++) - { - if (ch.ignoreEdge[j]) { continue; } - if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, ch.segments[j].End.Pos) < 1.0f) { continue; } - if (IsSegmentAInB(segments[i], ch.segments[j])) - { - ignoreEdge[i] = true; - if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].Start.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, false, segments[i].End.Pos); - } - else if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].End.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, false, segments[i].Start.Pos); - } - - if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].Start.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, true, segments[i].End.Pos); - } - else if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].End.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, true, segments[i].Start.Pos); - } - } - else if (IsSegmentAInB(ch.segments[j], segments[i])) - { - ch.ignoreEdge[j] = true; - - if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < 4.0f) - { - ShiftSegmentPoint(i, false, ch.segments[j].End.Pos); - } - else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < 4.0f) - { - ShiftSegmentPoint(i, false, ch.segments[j].Start.Pos); - } - - if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < 4.0f) - { - ShiftSegmentPoint(i, true, ch.segments[j].End.Pos); - } - else if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < 4.0f) - { - ShiftSegmentPoint(i, true, ch.segments[j].Start.Pos); - } + closest = intersection; } } + + losOffsets[i] = closest - losVertices[i].Pos; + overlappingHulls.Add(ch); + ch.overlappingHulls.Add(this); + changed = true; + } - - //ignore edges that are inside some other convex hull - for (int i = 0; i < vertices.Length; i++) + if (changed && refreshOtherOverlappingHulls) { - if (ch.IsPointInside(vertices[i].Pos)) + foreach (var overlapping in overlappingHulls) { - if (ch.IsPointInside(vertices[(i + 1) % vertices.Length].Pos)) - { - ignoreEdge[i] = true; - overlappingHulls.Add(ch); - } + overlapping.MergeLosVertices(this, refreshOtherOverlappingHulls: false); } } } - private void ShiftSegmentPoint(int segmentIndex, bool end, Vector2 newPos) - { - var segment = segments[segmentIndex]; - - losOffsets[segmentIndex] ??= new VectorPair(); - bool flipped = false; - if (Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.Start.Pos) > Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.End.Pos)) - { - flipped = true; - } - if (end == !flipped) - { - losOffsets[segmentIndex].B = newPos; - } - else - { - losOffsets[segmentIndex].A = newPos; - } - } - - public bool IsSegmentAInB(Segment a, Segment b) - { - if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos)) - { - return false; - } - - Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y)); - Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); - min.X -= 1.0f; min.Y -= 1.0f; - max.X += 1.0f; max.Y += 1.0f; - - if (a.Start.Pos.X < min.X) { return false; } - if (a.Start.Pos.Y < min.Y) { return false; } - if (a.End.Pos.X < min.X) { return false; } - if (a.End.Pos.Y < min.Y) { return false; } - - if (a.Start.Pos.X > max.X) { return false; } - if (a.Start.Pos.Y > max.Y) { return false; } - if (a.End.Pos.X > max.X) { return false; } - if (a.End.Pos.Y > max.Y) { return false; } - - float startDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.Start.Pos); - if (startDist > 1.0f) { return false; } - float endDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.End.Pos); - if (endDist > 1.0f) { return false; } - return true; - } - - public bool IsPointInside(Vector2 point) - { - if (!BoundingBox.Contains(point)) { return false; } - - Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f; - for (int i = 0; i < 4; i++) - { - Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos; - Vector2 centerToVertex = center - vertices[i].Pos; - Vector2 pointToVertex = point - vertices[i].Pos; - - float dotCenter = Vector2.Dot(centerToVertex, segmentVector); - float dotPoint = Vector2.Dot(pointToVertex, segmentVector); - - if ((dotCenter > 0f && dotPoint < 0f) || (dotCenter < 0f && dotPoint > 0f)) { return false; } - } - - return true; - } - - private void MergeSegments(Segment segment1, Segment segment2, bool startPointsMatch) - { - int startPointIndex = -1, endPointIndex = -1; - for (int i = 0; i < vertices.Length; i++) - { - if (vertices[i].Pos.NearlyEquals(segment1.Start.Pos)) - startPointIndex = i; - else if (vertices[i].Pos.NearlyEquals(segment1.End.Pos)) - endPointIndex = i; - } - if (startPointIndex == -1 || endPointIndex == -1) { return; } - - int startPoint2Index = -1, endPoint2Index = -1; - for (int i = 0; i < segment2.ConvexHull.vertices.Length; i++) - { - if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.Start.Pos)) - startPoint2Index = i; - else if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.End.Pos)) - endPoint2Index = i; - } - if (startPoint2Index == -1 || endPoint2Index == -1) { return; } - - if (startPointsMatch) - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.Start.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.End.Pos) / 2.0f; - } - else - { - if (Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.Start.Pos) < - Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.End.Pos)) - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; - } - else - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - } - } - - overlappingHulls.Add(segment2.ConvexHull); - segment2.ConvexHull.overlappingHulls.Add(this); - } - public void Rotate(Vector2 origin, float amount) { Matrix rotationMatrix = Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateRotationZ(amount) * Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); - SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); + SetVertices(vertices.Select(v => v.Pos).ToArray(), losVertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); } private void CalculateDimensions() @@ -470,11 +268,10 @@ namespace Barotrauma.Lights for (int i = 1; i < vertices.Length; i++) { - if (vertices[i].Pos.X < minX) minX = vertices[i].Pos.X; - if (vertices[i].Pos.Y < minY) minY = vertices[i].Pos.Y; - - if (vertices[i].Pos.X > maxX) maxX = vertices[i].Pos.X; - if (vertices[i].Pos.Y > minY) maxY = vertices[i].Pos.Y; + minX = Math.Min(minX, vertices[i].Pos.X); + minY = Math.Min(minY, vertices[i].Pos.Y); + maxX = Math.Max(maxX, vertices[i].Pos.X); + maxY = Math.Max(maxY, vertices[i].Pos.Y); } BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); @@ -485,21 +282,17 @@ namespace Barotrauma.Lights for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos += amount; - losVertices[i].Pos += amount; - - losOffsets[i] = null; - segments[i].Start.Pos += amount; segments[i].End.Pos += amount; } + for (int i = 0; i < losVertices.Length; i++) + { + losVertices[i].Pos += amount; + } LastVertexChangeTime = (float)Timing.TotalTime; overlappingHulls.Clear(); - for (int i = 0; i < 4; i++) - { - ignoreEdge[i] = false; - } CalculateDimensions(); @@ -511,8 +304,8 @@ namespace Barotrauma.Lights overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); - ch.MergeOverlappingSegments(this); + MergeLosVertices(ch); + ch.MergeLosVertices(this); } } } @@ -525,23 +318,23 @@ namespace Barotrauma.Lights foreach (ConvexHull ch in chList.List) { ch.overlappingHulls.Clear(); - for (int i = 0; i < 4; i++) + for (int i = 0; i < ch.losOffsets.Length; i++) { - ch.ignoreEdge[i] = false; + ch.losOffsets[i] = Vector2.Zero; } } for (int i = 0; i < chList.List.Count; i++) { for (int j = i + 1; j < chList.List.Count; j++) { - chList.List[i].MergeOverlappingSegments(chList.List[j]); - chList.List[j].MergeOverlappingSegments(chList.List[i]); + chList.List[i].MergeLosVertices(chList.List[j]); + chList.List[j].MergeLosVertices(chList.List[i]); } } } } - public void SetVertices(Vector2[] points, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) + public void SetVertices(Vector2[] points, Vector2[] losPoints, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) { Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); @@ -549,39 +342,23 @@ namespace Barotrauma.Lights for (int i = 0; i < 4; i++) { - vertices[i] = new SegmentPoint(points[i], this); - losVertices[i] = new SegmentPoint(points[i], this); - losOffsets[i] = null; + vertices[i] = new SegmentPoint(points[i], this); } - - for (int i = 0; i < 4; i++) + for (int i = 0; i < 2; i++) { - ignoreEdge[i] = false; + losVertices[i] = new SegmentPoint(losPoints[i], this); } overlappingHulls.Clear(); - int margin = 0; - if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y)) - { - losVertices[0].Pos = new Vector2(points[0].X + margin, points[0].Y); - losVertices[1].Pos = new Vector2(points[1].X + margin, points[1].Y); - losVertices[2].Pos = new Vector2(points[2].X - margin, points[2].Y); - losVertices[3].Pos = new Vector2(points[3].X - margin, points[3].Y); - } - else - { - losVertices[0].Pos = new Vector2(points[0].X, points[0].Y + margin); - losVertices[1].Pos = new Vector2(points[1].X, points[1].Y - margin); - losVertices[2].Pos = new Vector2(points[2].X, points[2].Y - margin); - losVertices[3].Pos = new Vector2(points[3].X, points[3].Y + margin); - } - if (rotationMatrix.HasValue) { for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos = Vector2.Transform(vertices[i].Pos, rotationMatrix.Value); + } + for (int i = 0; i < losVertices.Length; i++) + { losVertices[i].Pos = Vector2.Transform(losVertices[i].Pos, rotationMatrix.Value); } } @@ -602,7 +379,7 @@ namespace Barotrauma.Lights overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); + MergeLosVertices(ch); } } } @@ -624,31 +401,17 @@ namespace Barotrauma.Lights /// /// Returns the segments that are facing towards viewPosition /// - public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments, bool ignoreEdges) + public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments) { for (int i = 0; i < 4; i++) { - if (ignoreEdge[i] && ignoreEdges) continue; - - Vector2 pos1 = vertices[i].WorldPos; - Vector2 pos2 = vertices[(i + 1) % 4].WorldPos; - - Vector2 middle = (pos1 + pos2) / 2; - - Vector2 L = viewPosition - middle; - - Vector2 N = new Vector2( - -(pos2.Y - pos1.Y), - pos2.X - pos1.X); - - if (Vector2.Dot(N, L) > 0) + if (IsSegmentFacing(vertices[i].WorldPos, vertices[(i + 1) % 4].WorldPos, viewPosition)) { visibleSegments.Add(segments[i]); } } } - public void RefreshWorldPositions() { for (int i = 0; i < 4; i++) @@ -676,34 +439,12 @@ namespace Barotrauma.Lights ShadowVertexCount = 0; - //compute facing of each edge, using N*L - for (int i = 0; i < 4; i++) + for (int i = 0; i < losVertices.Length; i++) { - if (ignoreEdge[i]) - { - backFacing[i] = false; - continue; - } - - Vector2 firstVertex = losVertices[i].Pos; - Vector2 secondVertex = losVertices[(i+1) % 4].Pos; - - Vector2 L = lightSourcePos - ((firstVertex + secondVertex) / 2.0f); - - Vector2 N = new Vector2( - -(secondVertex.Y - firstVertex.Y), - secondVertex.X - firstVertex.X); - - backFacing[i] = (Vector2.Dot(N, L) < 0); - } - - ShadowVertexCount = 0; - for (int i = 0; i < 4; i++) - { - if (!backFacing[i]) { continue; } int currentIndex = i; - Vector3 vertexPos0 = new Vector3(losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos, 0.0f); - Vector3 vertexPos1 = new Vector3(losOffsets[currentIndex]?.B ?? losVertices[(currentIndex + 1) % 4].Pos, 0.0f); + int nextIndex = (currentIndex + 1) % 2; + Vector3 vertexPos0 = new Vector3(losVertices[currentIndex].Pos + losOffsets[currentIndex], 0.0f); + Vector3 vertexPos1 = new Vector3(losVertices[nextIndex].Pos + losOffsets[nextIndex], 0.0f); if (Vector3.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } @@ -754,9 +495,24 @@ namespace Barotrauma.Lights ShadowVertexCount += 6; } + if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos)) + { + Array.Reverse(ShadowVertices); + } + CalculateLosPenumbraVertices(lightSourcePos); } + private static bool IsSegmentFacing(Vector2 segmentPos1, Vector2 segmentPos2, Vector2 viewPosition) + { + Vector2 segmentMid = (segmentPos1 + segmentPos2) / 2; + Vector2 segmentDiff = segmentPos2 - segmentPos1; + Vector2 segmentNormal = new Vector2(-segmentDiff.Y, segmentDiff.X); + + Vector2 viewDirection = viewPosition - segmentMid; + return Vector2.Dot(segmentNormal, viewDirection) > 0; + } + private void CalculateLosPenumbraVertices(Vector2 lightSourcePos) { Vector3 offset = Vector3.Zero; @@ -766,73 +522,101 @@ namespace Barotrauma.Lights } PenumbraVertexCount = 0; - for (int i = 0; i < 4; i++) + for (int i = 0; i < losVertices.Length; i++) { int currentIndex = i; - int prevIndex = (i + 3) % 4; - int nextIndex = (i + 1) % 4; - bool disjointed = losOffsets[i]?.A != null; - Vector2 vertexPos0 = losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos; - Vector2 vertexPos1 = losOffsets[currentIndex]?.B ?? losVertices[nextIndex].Pos; + int nextIndex = (i + 1) % 2; + Vector2 vertexPos0 = losVertices[currentIndex].Pos + losOffsets[currentIndex]; + Vector2 vertexPos1 = losVertices[nextIndex].Pos + losOffsets[nextIndex]; if (Vector2.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } + + Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); - if (backFacing[currentIndex] && (disjointed || (!backFacing[prevIndex]))) + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; - PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture - { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); - for (int j = 0; j < 2; j++) - { - PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - - PenumbraVertexCount += 3; + PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } - disjointed = losOffsets[i]?.B != null; - if (backFacing[currentIndex] && (disjointed || (!backFacing[nextIndex]))) + PenumbraVertexCount += 3; + + penumbraStart = new Vector3(vertexPos1, 0.0f); + + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - Vector3 penumbraStart = new Vector3(vertexPos1, 0.0f); + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; - PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture - { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); - for (int j = 0; j < 2; j++) - { - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - - PenumbraVertexCount += 3; + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } + + PenumbraVertexCount += 3; + } + } + + public void DebugDraw(SpriteBatch spriteBatch) + { + //RecalculateAll(Submarine.MainSub); + //RefreshWorldPositions(); + + DrawLine(losVertices[0].Pos, losVertices[1].Pos, Color.Gray * 0.5f, width: 3); + DrawLine(losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1], Color.LightGreen, width: 2); + DrawLine(GameMain.GameScreen.Cam.Position + Vector2.One * 1000, GameMain.GameScreen.Cam.Position - Vector2.One * 1000, Color.Magenta, width: 2); + + if (GameMain.LightManager.LightingEnabled) + { + for (int i = 0; i < vertices.Length; i++) + { + Vector2 start = vertices[i].Pos; + Vector2 end = vertices[(i + 1) % 4].Pos; + DrawLine( + start, + end, Color.Yellow * 0.5f, + width: 4); + } + } + + void DrawLine(Vector2 vertexPos0, Vector2 vertexPos1, Color color, int width) + { + if (ParentEntity != null && ParentEntity.Submarine != null) + { + vertexPos0 += ParentEntity.Submarine.DrawPosition; + vertexPos1 += ParentEntity.Submarine.DrawPosition; + } + Vector2 viewTargetPos = LightManager.ViewTarget.WorldPosition; + float alpha = IsSegmentFacing(vertexPos0, vertexPos1, viewTargetPos) ? 1.0f : 0.5f; + vertexPos0.Y = -vertexPos0.Y; + vertexPos1.Y = -vertexPos1.Y; + GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width); } } @@ -903,16 +687,13 @@ namespace Barotrauma.Lights { HullLists.Remove(chList); } - foreach (ConvexHull ch2 in overlappingHulls) + //create a new list because MergeLosVertices can edit overlappingHulls + foreach (ConvexHull ch2 in overlappingHulls.ToList()) { - for (int i = 0; i < 4; i++) - { - ch2.ignoreEdge[i] = false; - } ch2.overlappingHulls.Remove(this); foreach (ConvexHull ch in chList.List) { - ch.MergeOverlappingSegments(ch2); + ch.MergeLosVertices(ch2); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index d8314b1b9..95a4d3a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Threading; namespace Barotrauma.Lights { @@ -22,6 +23,9 @@ namespace Barotrauma.Lights /// const float ObstructLightsBehindCharactersZoomThreshold = 0.5f; + private Thread rayCastThread; + private Queue pendingRayCasts = new Queue(); + public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -58,6 +62,8 @@ namespace Barotrauma.Lights private readonly List lights; + public bool DebugLos; + public bool LosEnabled = true; public float LosAlpha = 1f; public LosMode LosMode = LosMode.Transparent; @@ -68,6 +74,8 @@ namespace Barotrauma.Lights private readonly Texture2D visionCircle; + private readonly Texture2D gapGlowTexture; + private Vector2 losOffset; private int recalculationCount; @@ -85,8 +93,16 @@ namespace Barotrauma.Lights AmbientLight = new Color(20, 20, 20, 255); + rayCastThread = new Thread(UpdateRayCasts) + { + Name = "LightManager Raycast thread", + IsBackground = true //this should kill the thread if the game crashes + }; + rayCastThread.Start(); + visionCircle = Sprite.LoadTexture("Content/Lights/visioncircle.png"); highlightRaster = Sprite.LoadTexture("Content/UI/HighlightRaster.png"); + gapGlowTexture = Sprite.LoadTexture("Content/Lights/pointlight_rays.png"); GameMain.Instance.ResolutionChanged += () => { @@ -100,15 +116,12 @@ namespace Barotrauma.Lights LosEffect = EffectLoader.Load("Effects/losshader"); SolidColorEffect = EffectLoader.Load("Effects/solidcolor"); - if (lightEffect == null) - { - lightEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + lightEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true, TextureEnabled = true, Texture = LightSource.LightTexture }; - } }); } @@ -155,7 +168,7 @@ namespace Barotrauma.Lights { foreach (LightSource light in lights) { - light.NeedsHullCheck = true; + light.HullsUpToDate.Clear(); light.NeedsRecalculation = true; } } @@ -176,6 +189,51 @@ namespace Barotrauma.Lights } } + private class RayCastTask + { + public LightSource LightSource; + public Vector2 DrawPos; + public float Rotation; + + public RayCastTask(LightSource lightSource, Vector2 drawPos, float rotation) + { + LightSource = lightSource; + DrawPos = drawPos; + Rotation = rotation; + } + + public void Calculate() + { + LightSource.RayCastTask(DrawPos, Rotation); + } + } + + private static readonly object mutex = new object(); + + public void AddRayCastTask(LightSource lightSource, Vector2 drawPos, float rotation) + { + lock (mutex) + { + if (pendingRayCasts.Any(p => p.LightSource == lightSource)) { return; } + pendingRayCasts.Enqueue(new RayCastTask(lightSource, drawPos, rotation)); + } + } + + private void UpdateRayCasts() + { + while (true) + { + lock (mutex) + { + while (pendingRayCasts.Count > 0) + { + pendingRayCasts.Dequeue().Calculate(); + } + } + Thread.Sleep(10); + } + } + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -287,8 +345,8 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } - light.DrawSprite(spriteBatch, cam); light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); + light.DrawSprite(spriteBatch, cam); } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -307,15 +365,46 @@ namespace Barotrauma.Lights } spriteBatch.End(); - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(AmbientLight.Opaque().ToVector4()); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); - Submarine.DrawDamageable(spriteBatch, null); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); + Vector3 glowColorHSV = ToolBox.RGBToHSV(AmbientLight); + glowColorHSV.Z = Math.Max(glowColorHSV.Z, 0.4f); + Color glowColor = ToolBox.HSVToRGB(glowColorHSV.X, glowColorHSV.Y, glowColorHSV.Z); + Vector2 glowSpriteSize = new Vector2(gapGlowTexture.Width, gapGlowTexture.Height); + foreach (var gap in Gap.GapList) + { + if (gap.IsRoomToRoom || gap.Open <= 0.0f || gap.ConnectedWall == null) { continue; } + + float a = MathHelper.Lerp(0.5f, 1.0f, + PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.05f, gap.GlowEffectT)); + + float scale = MathHelper.Lerp(0.5f, 2.0f, + PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, gap.GlowEffectT)); + + float rot = PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.001f, gap.GlowEffectT) * MathHelper.TwoPi; + + Vector2 spriteScale = new Vector2(gap.Rect.Width, gap.Rect.Height) / glowSpriteSize; + Vector2 drawPos = new Vector2(gap.DrawPosition.X, -gap.DrawPosition.Y); + + spriteBatch.Draw(gapGlowTexture, + drawPos, + null, + glowColor * a, + rot, + glowSpriteSize / 2, + scale: Math.Max(spriteScale.X, spriteScale.Y) * scale, + SpriteEffects.None, + layerDepth: 0); + } + spriteBatch.End(); + + GameMain.GameScreen.DamageEffect.CurrentTechnique = GameMain.GameScreen.DamageEffect.Techniques["StencilShaderSolidColor"]; + GameMain.GameScreen.DamageEffect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: spriteBatchTransform, effect: GameMain.GameScreen.DamageEffect); + Submarine.DrawDamageable(spriteBatch, GameMain.GameScreen.DamageEffect); spriteBatch.End(); graphics.BlendState = BlendState.Additive; - //draw the focused item and character to highlight them, //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //--------------------------------------------------------------------------------------------------- @@ -449,9 +538,9 @@ namespace Barotrauma.Lights { highlightedEntities.Add(Character.Controlled.FocusedCharacter); } - foreach (Item item in Item.ItemList) + foreach (MapEntity me in MapEntity.HighlightedEntities) { - if ((item.IsHighlighted || item.IconStyle != null) && !highlightedEntities.Contains(item)) + if (me is Item item && item != Character.Controlled.FocusedItem) { highlightedEntities.Add(item); } @@ -565,7 +654,7 @@ namespace Barotrauma.Lights public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition) { - if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) return; + if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) { return; } if (ViewTarget == null) return; graphics.SetRenderTarget(LosTexture); @@ -597,23 +686,30 @@ namespace Barotrauma.Lights if (LosEnabled && LosMode != LosMode.None && ViewTarget != null) { Vector2 pos = ViewTarget.DrawPosition; + if (ViewTarget is Character character && + character.AnimController?.GetLimb(LimbType.Head) is Limb head && + !head.IsSevered && !head.Removed) + { + pos = head.body.DrawPosition; + } Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); Matrix shadowTransform = cam.ShaderTransform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; - var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width*0.75f, ViewTarget.Submarine); + var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget.Submarine); if (convexHulls != null) { List shadowVerts = new List(); List penumbraVerts = new List(); foreach (ConvexHull convexHull in convexHulls) { - if (!convexHull.Enabled || !convexHull.Intersects(camView)) continue; + if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } + if (LosMode == LosMode.BlockOutsideView && !convexHull.IsExteriorWall) { continue; }; Vector2 relativeLightPos = pos; - if (convexHull.ParentEntity?.Submarine != null) relativeLightPos -= convexHull.ParentEntity.Submarine.Position; + if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } convexHull.CalculateLosVertices(relativeLightPos); @@ -646,6 +742,20 @@ namespace Barotrauma.Lights graphics.SetRenderTarget(null); } + public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam) + { + if (ViewTarget == null) { return; } + spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform); + var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine); + Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); + foreach (ConvexHull convexHull in convexHulls) + { + if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } + convexHull.DebugDraw(spriteBatch); + } + spriteBatch.End(); + } + public void ClearLights() { lights.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 7f981e74f..d4aa9dd17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -15,6 +15,7 @@ namespace Barotrauma.Lights public bool Persistent; + public Dictionary SerializableProperties { get; private set; } = new Dictionary(); [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable] @@ -52,6 +53,10 @@ namespace Barotrauma.Lights [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ + " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] + public bool Directional { get; set; } + public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation))); private float flicker; @@ -203,7 +208,7 @@ namespace Barotrauma.Lights private VertexPositionColorTexture[] vertices; private short[] indices; - private readonly List hullsInRange; + private readonly List convexHullsInRange; public Texture2D texture; @@ -222,9 +227,10 @@ namespace Barotrauma.Lights private float prevCalculatedRange; private Vector2 prevCalculatedPosition; - //do we need to recheck which convex hulls are within range - //(e.g. position or range of the lightsource has changed) - public bool NeedsHullCheck = true; + //Which submarines' convex hulls are up to date? Resets when the item moves/rotates relative to the submarine. + //Can contain null (means convex hulls that aren't part of any submarine). + public HashSet HullsUpToDate = new HashSet(); + //do we need to recalculate the vertices of the light volume private bool needsRecalculation; public bool NeedsRecalculation @@ -234,7 +240,7 @@ namespace Barotrauma.Lights { if (!needsRecalculation && value) { - foreach (ConvexHullList chList in hullsInRange) + foreach (ConvexHullList chList in convexHullsInRange) { chList.IsHidden.Clear(); } @@ -246,6 +252,18 @@ namespace Barotrauma.Lights //when were the vertices of the light volume last calculated public float LastRecalculationTime { get; private set; } + + private enum LightVertexState + { + UpToDate, + PendingRayCasts, + PendingVertexRecalculation, + } + + private LightVertexState state; + + private Vector2 calculatedDrawPos; + private readonly Dictionary diffToSub; private DynamicVertexBuffer lightVolumeBuffer; @@ -254,7 +272,6 @@ namespace Barotrauma.Lights private int indexCount; private Vector2 translateVertices; - private float rotateVertices; private readonly LightSourceParams lightSourceParams; @@ -277,7 +294,7 @@ namespace Barotrauma.Lights return; } - NeedsHullCheck = true; + HullsUpToDate.Clear(); NeedsRecalculation = true; } } @@ -292,17 +309,20 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; + dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); + if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { - rotateVertices = rotation - prevCalculatedRotation; return; } - NeedsHullCheck = true; + HullsUpToDate.Clear(); NeedsRecalculation = true; } } + private Vector2 dir = Vector2.UnitX; + private Vector2 _spriteScale = Vector2.One; public Vector2 SpriteScale @@ -368,7 +388,7 @@ namespace Barotrauma.Lights lightSourceParams.Range = value; if (Math.Abs(prevCalculatedRange - lightSourceParams.Range) < 10.0f) return; - NeedsHullCheck = true; + HullsUpToDate.Clear(); NeedsRecalculation = true; prevCalculatedRange = lightSourceParams.Range; } @@ -384,8 +404,8 @@ namespace Barotrauma.Lights set { NeedsRecalculation = true; - NeedsHullCheck = true; lightTextureTargetSize = value; + HullsUpToDate.Clear(); } } @@ -424,7 +444,7 @@ namespace Barotrauma.Lights public bool Enabled = true; private readonly ISerializableEntity conditionalTarget; - private readonly PropertyConditional.Comparison comparison; + private readonly PropertyConditional.LogicalOperatorType logicalOperator; private readonly List conditionals = new List(); public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) @@ -432,11 +452,8 @@ namespace Barotrauma.Lights { lightSourceParams = new LightSourceParams(element); CastShadows = element.GetAttributeBool("castshadows", true); - string comparison = element.GetAttributeString("comparison", null); - if (comparison != null) - { - Enum.TryParse(comparison, ignoreCase: true, out this.comparison); - } + logicalOperator = element.GetAttributeEnum(nameof(logicalOperator), + element.GetAttributeEnum("comparison", logicalOperator)); if (lightSourceParams.DeformableLightSpriteElement != null) { @@ -449,13 +466,7 @@ namespace Barotrauma.Lights switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } @@ -474,7 +485,7 @@ namespace Barotrauma.Lights public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true) { - hullsInRange = new List(); + convexHullsInRange = new List(); this.ParentSub = submarine; this.position = position; lightSourceParams = new LightSourceParams(range, color); @@ -515,19 +526,46 @@ namespace Barotrauma.Lights /// private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub) { - var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); + var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } - chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); + //used to check whether the lightsource hits the target hull if the light is directional + Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange; + Vector2 normal = new Vector2(-ray.Y, ray.X); - NeedsHullCheck = true; + chList.List.Clear(); + foreach (var convexHull in fullChList.List) + { + if (!convexHull.Enabled) { continue; } + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } + if (lightSourceParams.Directional && false) + { + Rectangle bounds = convexHull.BoundingBox; + //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left + bounds.Y -= bounds.Height; + + //the ray can't hit if + // center is in the opposite direction from the ray (cheapest check first) + if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 && + /*ray doesn't hit the convex hull*/ + !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) && + /*normal vectors of the ray don't hit the convex hull */ + !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _)) + { + continue; + } + } + chList.List.Add(convexHull); + } + chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); + HullsUpToDate.Add(sub); } /// /// Recheck which convex hulls are in range (if needed), /// and check if we need to recalculate vertices due to changes in the convex hulls /// - private void CheckHullsInRange() + private void CheckConvexHullsInRange() { foreach (Submarine sub in Submarine.Loaded) { @@ -540,21 +578,13 @@ namespace Barotrauma.Lights private void CheckHullsInRange(Submarine sub) { //find the list of convexhulls that belong to the sub - ConvexHullList chList = null; - foreach (var ch in hullsInRange) - { - if (ch.Submarine == sub) - { - chList = ch; - break; - } - } - + ConvexHullList chList = convexHullsInRange.FirstOrDefault(chList => chList.Submarine == sub); + //not found -> create one if (chList == null) { chList = new ConvexHullList(sub); - hullsInRange.Add(chList); + convexHullsInRange.Add(chList); NeedsRecalculation = true; } @@ -573,7 +603,7 @@ namespace Barotrauma.Lights //light and the convexhulls are both outside if (sub == null) { - if (NeedsHullCheck) + if (!HullsUpToDate.Contains(null)) { RefreshConvexHullList(chList, lightPos, null); } @@ -605,7 +635,7 @@ namespace Barotrauma.Lights //light and convexhull are both inside the same sub if (sub == ParentSub) { - if (NeedsHullCheck) + if (!HullsUpToDate.Contains(sub)) { RefreshConvexHullList(chList, lightPos, sub); } @@ -613,7 +643,7 @@ namespace Barotrauma.Lights //light and convexhull are inside different subs else { - if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) { return; } + if (sub.DockedTo.Contains(ParentSub) && HullsUpToDate.Contains(sub)) { return; } lightPos -= (sub.Position - ParentSub.Position); @@ -646,19 +676,45 @@ namespace Barotrauma.Lights } } - private List FindRaycastHits() + private static readonly object mutex = new object(); + + private readonly List visibleSegments = new List(); + private readonly List points = new List(); + private readonly List verts = new List(); + private readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; + private void FindRaycastHits() { - if (!CastShadows || Range < 1.0f || Color.A < 1) { return null; } + if (!CastShadows || Range < 1.0f || Color.A < 1) + { + state = LightVertexState.PendingVertexRecalculation; + return; + } Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - var hulls = new List(); - foreach (ConvexHullList chList in hullsInRange) + visibleSegments.Clear(); + foreach (ConvexHullList chList in convexHullsInRange) { foreach (ConvexHull hull in chList.List) { - if (!chList.IsHidden.Contains(hull)) { hulls.Add(hull); } + if (!chList.IsHidden.Contains(hull)) + { + //find convexhull segments that are close enough and facing towards the light source + lock (mutex) + { + hull.RefreshWorldPositions(); + hull.GetVisibleSegments(drawPos, visibleSegments); + foreach (var visibleSegment in visibleSegments) + { + if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null) + { + visibleSegment.SubmarineDrawPos = visibleSegment.ConvexHull.ParentEntity.Submarine.DrawPosition; + } + } + } + + } } foreach (ConvexHull hull in chList.List) { @@ -666,27 +722,19 @@ namespace Barotrauma.Lights } } - float bounds = TextureRange; - //find convexhull segments that are close enough and facing towards the light source - List visibleSegments = new List(); - List points = new List(); - foreach (ConvexHull hull in hulls) - { - hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); - } + state = LightVertexState.PendingRayCasts; + GameMain.LightManager.AddRayCastTask(this, drawPos, rotation); + } - //add a square-shaped boundary to make sure we've got something to construct the triangles from - //even if there aren't enough hull segments around the light source - - //(might be more effective to calculate if we actually need these extra points) + public void RayCastTask(Vector2 drawPos, float rotation) + { Vector2 drawOffset = Vector2.Zero; - float boundsExtended = bounds; + float boundsExtended = TextureRange; if (OverrideLightTexture != null) { - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); + float cosAngle = (float)Math.Cos(rotation); + float sinAngle = -(float)Math.Sin(rotation); var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -706,12 +754,16 @@ namespace Barotrauma.Lights drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; } - var boundaryCorners = new SegmentPoint[] { - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null) - }; + //add a square-shaped boundary to make sure we've got something to construct the triangles from + //even if there aren't enough hull segments around the light source + + //(might be more effective to calculate if we actually need these extra points) + Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended); + Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended); + boundaryCorners[0] = new SegmentPoint(boundsMax, null); + boundaryCorners[1] = new SegmentPoint(new Vector2(boundsMax.X, boundsMin.Y), null); + boundaryCorners[2] = new SegmentPoint(boundsMin, null); + boundaryCorners[3] = new SegmentPoint(new Vector2(boundsMin.X, boundsMax.Y), null); for (int i = 0; i < 4; i++) { @@ -719,199 +771,200 @@ namespace Barotrauma.Lights visibleSegments.Add(s); } - //Generate new points at the intersections between segments - //This is necessary for the light volume to generate properly on some subs - for (int i = 0; i < visibleSegments.Count; i++) + lock (mutex) { - Vector2 p1a = visibleSegments[i].Start.WorldPos; - Vector2 p1b = visibleSegments[i].End.WorldPos; - - for (int j = i + 1; j < visibleSegments.Count; j++) + //Generate new points at the intersections between segments + //This is necessary for the light volume to generate properly on some subs + for (int i = 0; i < visibleSegments.Count; i++) { - //ignore intersections between parallel axis-aligned segments - if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && - visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal) - { - continue; - } + Vector2 p1a = visibleSegments[i].Start.WorldPos; + Vector2 p1b = visibleSegments[i].End.WorldPos; - Vector2 p2a = visibleSegments[j].Start.WorldPos; - Vector2 p2b = visibleSegments[j].End.WorldPos; - - if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || - Vector2.DistanceSquared(p1a, p2b) < 5.0f || - Vector2.DistanceSquared(p1b, p2a) < 5.0f || - Vector2.DistanceSquared(p1b, p2b) < 5.0f) + for (int j = i + 1; j < visibleSegments.Count; j++) { - continue; - } - - bool intersects; - Vector2 intersection = Vector2.Zero; - if (visibleSegments[i].IsAxisAligned) - { - intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection); - } - else if (visibleSegments[j].IsAxisAligned) - { - intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection); - } - else - { - intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); - } - - if (intersects) - { - SegmentPoint start = visibleSegments[i].Start; - SegmentPoint end = visibleSegments[i].End; - SegmentPoint mid = new SegmentPoint(intersection, null); - if (visibleSegments[i].ConvexHull?.ParentEntity?.Submarine != null) - { - mid.Pos -= visibleSegments[i].ConvexHull.ParentEntity.Submarine.DrawPosition; - } - - if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || - Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) + //ignore intersections between parallel axis-aligned segments + if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && + visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal) { continue; } - Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) - { - IsHorizontal = visibleSegments[i].IsHorizontal, - }; + Vector2 p2a = visibleSegments[j].Start.WorldPos; + Vector2 p2b = visibleSegments[j].End.WorldPos; - Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) + if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || + Vector2.DistanceSquared(p1a, p2b) < 5.0f || + Vector2.DistanceSquared(p1b, p2a) < 5.0f || + Vector2.DistanceSquared(p1b, p2b) < 5.0f) { - IsHorizontal = visibleSegments[i].IsHorizontal - }; + continue; + } - visibleSegments[i] = seg1; - visibleSegments.Insert(i + 1, seg2); + bool intersects; + Vector2 intersection = Vector2.Zero; + if (visibleSegments[i].IsAxisAligned) + { + intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection); + } + else if (visibleSegments[j].IsAxisAligned) + { + intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection); + } + else + { + intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); + } + + if (intersects) + { + SegmentPoint start = visibleSegments[i].Start; + SegmentPoint end = visibleSegments[i].End; + SegmentPoint mid = new SegmentPoint(intersection, null); + mid.Pos -= visibleSegments[i].SubmarineDrawPos; + + if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || + Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) + { + continue; + } + + Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) + { + IsHorizontal = visibleSegments[i].IsHorizontal, + }; + + Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) + { + IsHorizontal = visibleSegments[i].IsHorizontal + }; + + visibleSegments[i] = seg1; + visibleSegments.Insert(i + 1, seg2); + i--; + break; + } + } + } + + points.Clear(); + //remove segments that fall out of bounds + for (int i = 0; i < visibleSegments.Count; i++) + { + Segment s = visibleSegments[i]; + if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + { + visibleSegments.RemoveAt(i); i--; - break; + } + else + { + points.Add(s.Start); + points.Add(s.End); } } - } - //remove segments that fall out of bounds - for (int i = 0; i < visibleSegments.Count; i++) - { - Segment s = visibleSegments[i]; - if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || - Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || - Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || - Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + //remove points that are very close to each other + for (int i = 0; i < points.Count; i++) { - visibleSegments.RemoveAt(i); - i--; + for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) + { + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + { + points.RemoveAt(j); + } + } } - else + + var compareCCW = new CompareSegmentPointCW(drawPos); + try { - points.Add(s.Start); - points.Add(s.End); + points.Sort(compareCCW); } - } - - visibleSegments = visibleSegments.OrderBy(s => MathUtils.LineToPointDistanceSquared(s.Start.WorldPos, s.End.WorldPos, drawPos)).ToList(); - - var compareCCW = new CompareSegmentPointCW(drawPos); - try - { - points.Sort(compareCCW); - } - catch (Exception e) - { - StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); - foreach (SegmentPoint sp in points) + catch (Exception e) { - sb.AppendLine(sp.Pos.ToString()); + StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); + foreach (SegmentPoint sp in points) + { + sb.AppendLine(sp.Pos.ToString()); + } + DebugConsole.ThrowError(sb.ToString(), e); } - DebugConsole.ThrowError(sb.ToString(), e); - } - List output = new List(); - //List> preOutput = new List>(); + visibleSegments.Sort((s1, s2) => + MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos) + .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos))); + + verts.Clear(); + foreach (SegmentPoint p in points) + { + Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); + Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; + + //do two slightly offset raycasts to hit the segment itself and whatever's behind it + var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); + if (intersection1.index < 0) { return; } + var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); + if (intersection2.index < 0) { return; } + + Segment seg1 = visibleSegments[intersection1.index]; + Segment seg2 = visibleSegments[intersection2.index]; + + bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; + bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; + + if (isPoint1 && isPoint2) + { + //hit at the current segmentpoint -> place the segmentpoint into the list + verts.Add(p.WorldPos); + + foreach (ConvexHullList hullList in convexHullsInRange) + { + hullList.IsHidden.Remove(p.ConvexHull); + hullList.IsHidden.Remove(seg1.ConvexHull); + hullList.IsHidden.Remove(seg2.ConvexHull); + } + } + else if (intersection1.index != intersection2.index) + { + //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); + + foreach (ConvexHullList hullList in convexHullsInRange) + { + hullList.IsHidden.Remove(p.ConvexHull); + hullList.IsHidden.Remove(seg1.ConvexHull); + hullList.IsHidden.Remove(seg2.ConvexHull); + } + } + //if neither of the conditions above are met, we just assume + //that the raycasts both resulted on the same segment + //and creating geometry here would be wasteful + } + } //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) + for (int i = 0; i < verts.Count - 1; i++) { - for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) + for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--) { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + if (Math.Abs(verts[i].X - verts[j].X) < 6 && + Math.Abs(verts[i].Y - verts[j].Y) < 6) { - points.RemoveAt(j); + verts.RemoveAt(j); } } } - - foreach (SegmentPoint p in points) - { - Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); - Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; - - //do two slightly offset raycasts to hit the segment itself and whatever's behind it - var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); - var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); - - if (intersection1.index < 0) return null; - if (intersection2.index < 0) return null; - Segment seg1 = visibleSegments[intersection1.index]; - Segment seg2 = visibleSegments[intersection2.index]; - - bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; - bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; - - if (isPoint1 && isPoint2) - { - //hit at the current segmentpoint -> place the segmentpoint into the list - output.Add(p.WorldPos); - - foreach (ConvexHullList hullList in hullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } - } - else if (intersection1.index != intersection2.index) - { - //the raycasts landed on different segments - //we definitely want to generate new geometry here - output.Add(isPoint1 ? p.WorldPos : intersection1.pos); - output.Add(isPoint2 ? p.WorldPos : intersection2.pos); - - foreach (ConvexHullList hullList in hullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } - } - //if neither of the conditions above are met, we just assume - //that the raycasts both resulted on the same segment - //and creating geometry here would be wasteful - } - - //remove points that are very close to each other - for (int i = 0; i < output.Count - 1; i++) - { - for (int j = Math.Min(i + 4, output.Count - 1); j > i; j--) - { - if (Math.Abs(output[i].X - output[j].X) < 6 && - Math.Abs(output[i].Y - output[j].Y) < 6) - { - output.RemoveAt(j); - } - } - } - - return output; + calculatedDrawPos = drawPos; + state = LightVertexState.PendingVertexRecalculation; } - private (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) + private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) { Vector2? closestIntersection = null; int segment = -1; @@ -936,13 +989,13 @@ namespace Barotrauma.Lights //same for the x-axis if (s.Start.WorldPos.X > s.End.WorldPos.X) { - if (s.Start.WorldPos.X < minX) continue; - if (s.End.WorldPos.X > maxX) continue; + if (s.Start.WorldPos.X < minX) { continue; } + if (s.End.WorldPos.X > maxX) { continue; } } else { - if (s.End.WorldPos.X < minX) continue; - if (s.Start.WorldPos.X > maxX) continue; + if (s.End.WorldPos.X < minX) { continue; } + if (s.Start.WorldPos.X > maxX) { continue; } } bool intersects; @@ -986,14 +1039,11 @@ namespace Barotrauma.Lights indices = new short[indexCount]; } - Vector2 drawPos = position; - if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); + Vector2 drawPos = calculatedDrawPos; Vector2 uvOffset = Vector2.Zero; Vector2 overrideTextureDims = Vector2.One; + Vector2 dir = this.dir; if (OverrideLightTexture != null) { overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -1002,8 +1052,7 @@ namespace Barotrauma.Lights if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; - cosAngle = -cosAngle; - sinAngle = -sinAngle; + dir = -dir; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); @@ -1041,7 +1090,7 @@ namespace Barotrauma.Lights //calculate normal of first segment Vector2 nDiff1 = vertex - nextVertex; - float tx = nDiff1.X; nDiff1.X = -nDiff1.Y; nDiff1.Y = tx; + nDiff1 = new Vector2(-nDiff1.Y, nDiff1.X); nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y)); //if the normal is pointing towards the light origin //rather than away from it, invert it @@ -1049,21 +1098,23 @@ namespace Barotrauma.Lights //calculate normal of second segment Vector2 nDiff2 = prevVertex - vertex; - tx = nDiff2.X; nDiff2.X = -nDiff2.Y; nDiff2.Y = tx; - nDiff2 /= Math.Max(Math.Abs(nDiff2.X),Math.Abs(nDiff2.Y)); + nDiff2 = new Vector2(-nDiff2.Y, nDiff2.X); + nDiff2 /= Math.Max(Math.Abs(nDiff2.X), Math.Abs(nDiff2.Y)); //if the normal is pointing towards the light origin //rather than away from it, invert it if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2; //add the normals together and use some magic numbers to create //a somewhat useful/good-looking blur - Vector2 nDiff = nDiff1 * 40.0f; - if (MathUtils.GetLineIntersection(vertex + (nDiff1 * 40.0f), nextVertex + (nDiff1 * 40.0f), vertex + (nDiff2 * 40.0f), prevVertex + (nDiff2 * 40.0f), true, out Vector2 intersection)) + float blurDistance = 40.0f; + Vector2 nDiff = nDiff1 * blurDistance; + if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection)) { nDiff = intersection - vertex; - if (nDiff.LengthSquared() > 10000.0f) + if (nDiff.LengthSquared() > 100.0f * 100.0f) { - nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); nDiff *= 100.0f; + nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); + nDiff *= 100.0f; } } @@ -1074,8 +1125,8 @@ namespace Barotrauma.Lights //calculate texture coordinates based on the light's rotation Vector2 originDiff = diff; - diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle; - diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle; + diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y; + diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X; diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); diff += uvOffset; } @@ -1161,7 +1212,6 @@ namespace Barotrauma.Lights } translateVertices = Vector2.Zero; - rotateVertices = 0.0f; prevCalculatedPosition = position; prevCalculatedRotation = rotation; } @@ -1181,9 +1231,6 @@ namespace Barotrauma.Lights } drawPos.Y = -drawPos.Y; - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); - float bounds = TextureRange; if (OverrideLightTexture != null) @@ -1195,8 +1242,8 @@ namespace Barotrauma.Lights origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin *= TextureRange; - drawPos.X += origin.X * sinAngle + origin.Y * cosAngle; - drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle; + drawPos.X += origin.X * dir.Y + origin.Y * dir.X; + drawPos.Y += origin.X * dir.X + origin.Y * dir.Y; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1302,7 +1349,7 @@ namespace Barotrauma.Lights { if (conditionals.None()) { return; } if (conditionalTarget == null) { return; } - if (comparison == PropertyConditional.Comparison.And) + if (logicalOperator == PropertyConditional.LogicalOperatorType.And) { Enabled = conditionals.All(c => c.Matches(conditionalTarget)); } @@ -1335,35 +1382,43 @@ namespace Barotrauma.Lights return; } - CheckHullsInRange(); + CheckConvexHullsInRange(); if (NeedsRecalculation && allowRecalculation) { - recalculationCount++; - var verts = FindRaycastHits(); - if (verts == null) + if (state == LightVertexState.UpToDate) { -#if DEBUG - DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); -#endif - Enabled = false; - return; + recalculationCount++; + FindRaycastHits(); } + else if (state == LightVertexState.PendingVertexRecalculation) + { + if (verts == null) + { + #if DEBUG + DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); + #endif + Enabled = false; + return; + } - CalculateLightVertices(verts); + CalculateLightVertices(verts); - LastRecalculationTime = (float)Timing.TotalTime; - NeedsRecalculation = false; + LastRecalculationTime = (float)Timing.TotalTime; + NeedsRecalculation = false; + state = LightVertexState.UpToDate; + } } + if (vertexCount == 0) { return; } + Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * - Matrix.CreateRotationZ(rotateVertices - MathHelper.ToRadians(LightSourceParams.Rotation)) * + Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * transform; - if (vertexCount == 0) { return; } lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f; if (OverrideLightTexture != null) @@ -1387,9 +1442,9 @@ namespace Barotrauma.Lights public void Reset() { - hullsInRange.Clear(); + HullsUpToDate.Clear(); + convexHullsInRange.Clear(); diffToSub.Clear(); - NeedsHullCheck = true; NeedsRecalculation = true; vertexCount = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 573c648c4..2170143c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -70,9 +70,9 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted || !(entity is Item) || !entity.IsMouseOn(position)) { continue; } + if (entity == this|| entity is not Item || !entity.IsMouseOn(position)) { continue; } if (((Item)entity).GetComponent() == null) { continue; } if (linkedTo.Contains(entity)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index bdc94b8de..783c6d66a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework.Input; +using Barotrauma.Extensions; namespace Barotrauma { @@ -72,6 +73,8 @@ namespace Barotrauma private RichString beaconStationActiveText, beaconStationInactiveText; + private GUIComponent locationInfoOverlay; + /*private (Rectangle targetArea, string tip)? connectionTooltip; private string sanitizedConnectionTooltip; private List connectionTooltipRichTextData; @@ -98,7 +101,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); - Generate(GameMain.GameSession.GameMode is CampaignMode campaign ? campaign.Settings : CampaignSettings.Empty); + Generate(GameMain.GameSession?.Campaign); InitProjectSpecific(); return true; } @@ -186,7 +189,7 @@ namespace Barotrauma private void LocationChanged(Location prevLocation, Location newLocation) { - if (prevLocation == newLocation) return; + if (prevLocation == newLocation) { return; } //focus on starting location if (prevLocation != null) { @@ -210,11 +213,17 @@ namespace Barotrauma currLocationIndicatorPos = CurrentLocation.MapPosition; } - RemoveFogOfWar(newLocation); + if (newLocation.Visited) + { + RemoveFogOfWar(newLocation); + } } + partial void RemoveFogOfWarProjSpecific(Location location) => RemoveFogOfWar(location); + private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) { + if (mapTiles == null) { return; } if (location == null) { return; } var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault(); @@ -252,27 +261,223 @@ namespace Barotrauma return !tileDiscovered[MathHelper.Clamp(x, 0, tileDiscovered.Length), MathHelper.Clamp(y, 0, tileDiscovered.Length)]; } + private class MapNotification + { + public readonly RichString Text; + public readonly GUIFont Font; + + public readonly Vector2 TextSize; + + public int TimesShown; + + public float Offset; + + public readonly Location RelatedLocation; + + public bool IsCurrentlyVisible; + + public MapNotification(string text, GUIFont font, List existingNotifications, Location relatedLocation) + { + Text = RichString.Rich(text); + Font = font; + TextSize = Font.MeasureString(Font.ForceUpperCase ? Text.SanitizedValue.ToUpper() : Text.SanitizedValue); + if (existingNotifications.Any()) + { + Offset = existingNotifications.Max(n => n.Offset + n.TextSize.X + GUI.IntScale(60)); + } + RelatedLocation = relatedLocation; + } + } + + private readonly List mapNotifications = new List(); + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) { - if (change.Messages.Any()) + 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‖"); + location.LastTypeChangeMessage = msg; + + mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location)); + } + + public void DrawNotifications(SpriteBatch spriteBatch, GUICustomComponent container) + { + Vector2 pos = new Vector2(container.Rect.Right, container.Rect.Center.Y); + foreach (var notification in mapNotifications) { - string msg = change.Messages[Rand.Range(0, change.Messages.Count)] - .Replace("[previousname]", $"‖color:gui.orange‖{prevName}‖end‖") - .Replace("[name]", $"‖color:gui.orange‖{location.Name}‖end‖"); - location.LastTypeChangeMessage = msg; - if (GameMain.Client != null) + Vector2 textPos = pos + new Vector2(notification.Offset, -notification.TextSize.Y / 2); + + notification.Font.DrawStringWithColors( + spriteBatch, + notification.Text.SanitizedValue, + textPos, + Color.White, 0.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0, + notification.Text.RichTextData); + + int margin = container.Rect.Width / 5; + notification.IsCurrentlyVisible = + textPos.X < container.Rect.Right - margin && + textPos.X + notification.TextSize.X > container.Rect.X + margin; + } + } + + private void UpdateNotifications(float deltaTime, GUICustomComponent mapContainer) + { + if (mapNotifications.Count < 5) + { + int maxIndex = 1; + while (TextManager.ContainsTag("randomnews" + maxIndex)) { - GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName").Value); + maxIndex++; } - else + string textTag = "randomnews" + Rand.Range(0, maxIndex); + if (TextManager.ContainsTag(textTag)) { - GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage( - TextManager.Get("RadioAnnouncerName").Value, - msg, - Networking.ChatMessageType.Default, - sender: null); + mapNotifications.Add(new MapNotification(TextManager.Get(textTag).Value, GUIStyle.SubHeadingFont, mapNotifications, relatedLocation: null)); } - } + } + + for (int i = mapNotifications.Count - 1; i >= 0; i--) + { + var notification = mapNotifications[i]; + notification.Offset -= deltaTime * 75.0f; + if (notification.Offset < -notification.TextSize.X - mapContainer.Rect.Width) + { + notification.Offset = Math.Max(mapNotifications.Max(n => n.Offset + n.TextSize.X) + GUI.IntScale(60), 0); + notification.TimesShown++; + if (mapNotifications.Count > 5) + { + mapNotifications.RemoveAt(i); + } + else if (mapNotifications.Count > 3 && notification.TimesShown > 2) + { + mapNotifications.RemoveAt(i); + } + } + } + } + + private void CreateLocationInfoOverlay(Location location) + { + locationInfoOverlay = new GUIFrame(new RectTransform(new Point(GUI.IntScale(350), GUI.IntScale(350)), GUI.Canvas), style: "GUIToolTip") + { + UserData = location + }; + locationInfoOverlay.Color *= 0.8f; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), locationInfoOverlay.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + 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 }; + 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 }; + } + + CreateSpacing(10); + + if (!location.Type.Description.IsNullOrEmpty()) + { + CreateTextWithIcon(location.Type.Description, location.Type.Sprite); + } + + int highestSubTier = location.HighestSubmarineTierAvailable(); + List<(SubmarineClass subClass, int tier)> overrideTiers = null; + if (location.CanHaveSubsForSale()) + { + overrideTiers = new List<(SubmarineClass subClass, int tier)>(); + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) + { + if (subClass == SubmarineClass.Undefined) { continue; } + int highestClassTier = location.HighestSubmarineTierAvailable(subClass); + if (highestClassTier > 0 && highestClassTier > highestSubTier) + { + overrideTiers.Add((subClass, highestClassTier)); + } + } + } + if (highestSubTier > 0) + { + CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + if (overrideTiers != null) + { + foreach (var (subClass, tier) in overrideTiers) + { + CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + } + + CreateSpacing(10); + + void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null) + { + var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true) + { + Stretch = true, + CanBeFocused = true + }; + var guiIcon = + style == null ? + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) : + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style); + var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text); + textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0); + textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0); + } + + void CreateSpacing(int height) + { + new GUIFrame(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(height)), content.RectTransform), style: null); + } + + if (location.Faction != null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + RichString.Rich(TextManager.GetWithVariables("reputationgainnotification", + ("[value]", string.Empty), + ("[reputationname]", $"‖color:{XMLExtensions.ToStringHex(location.Faction.Prefab.IconColor)}‖{location.Faction.Prefab.Name}‖end‖")))) + { + Padding = Vector4.Zero + }; + + CreateSpacing(10); + + var repBarHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(25)), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => + { + if (location.Reputation == null) { return; } + RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); + }); + + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform), + location.Reputation.GetFormattedReputationText(), textAlignment: Alignment.CenterRight); + + new GUIImage(new RectTransform(new Vector2(0.25f, 0.5f), locationInfoOverlay.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.05f) }, + location.Faction.Prefab.Icon, scaleToFit: true) + { + Color = location.Faction.Prefab.IconColor * 0.5f + }; + CreateSpacing(20); + } + + locationInfoOverlay.RectTransform.NonScaledSize = + new Point( + Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)), + (int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y)); } partial void ClearAnimQueue() @@ -280,12 +485,13 @@ namespace Barotrauma mapAnimQueue.Clear(); } - public void Update(float deltaTime, GUICustomComponent mapContainer) + public void Update(CampaignMode campaign, float deltaTime, GUICustomComponent mapContainer) { Rectangle rect = mapContainer.Rect; - var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + UpdateNotifications(deltaTime, mapContainer); + var currentDisplayLocation = campaign?.GetCurrentDisplayLocation(); if (currentDisplayLocation != null) { if (!currentDisplayLocation.Discovered) @@ -345,10 +551,39 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 viewOffset = DrawOffset + drawOffsetNoise; + if (HighlightedLocation != null) + { + Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; + if (locationInfoOverlay == null || locationInfoOverlay.UserData != HighlightedLocation) + { + CreateLocationInfoOverlay(HighlightedLocation); + } + + Point offsetFromLocationIcon = new Point(GUI.IntScale(25)); + var locationInfoRt = locationInfoOverlay.RectTransform; + if (locationInfoRt.Pivot == Pivot.BottomLeft || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.Y = -offsetFromLocationIcon.Y; + } + if (locationInfoRt.Pivot == Pivot.TopRight || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.X = -offsetFromLocationIcon.X; + } + locationInfoRt.ScreenSpaceOffset = highlightedLocationDrawPos.ToPoint() + offsetFromLocationIcon; + if (locationInfoOverlay.Rect.Bottom > rect.Bottom) + { + locationInfoRt.Pivot = Pivot.BottomLeft; + } + if (locationInfoOverlay.Rect.Right > rect.Right) + { + locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight; + } + locationInfoOverlay?.AddToGUIUpdateList(order: 1); + } float closestDist = 0.0f; HighlightedLocation = null; - if (GUI.MouseOn == null || GUI.MouseOn == mapContainer) + if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer)) { for (int i = 0; i < Locations.Count; i++) { @@ -374,7 +609,7 @@ namespace Barotrauma if (HighlightedLocation == null || dist < closestDist) { closestDist = dist; - HighlightedLocation = location; + HighlightedLocation = location; } } } @@ -453,12 +688,13 @@ namespace Barotrauma Level.Loaded.DebugSetEndLocation(null); Discover(CurrentLocation); + Visit(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) { CurrentLocation.CreateStores(); - ProgressWorld(); + ProgressWorld(campaign); Radiation?.OnStep(1); } else @@ -467,12 +703,6 @@ namespace Barotrauma } } - if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) && PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation != null) - { - int distance = DistanceToClosestLocationWithOutpost(HighlightedLocation, out Location foundLocation); - DebugConsole.NewMessage($"Distance to closest outpost from {HighlightedLocation.Name} to {foundLocation?.Name} is {distance}"); - } - if (PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation == null) { SelectLocation(-1); @@ -481,10 +711,10 @@ namespace Barotrauma } } - public void Draw(SpriteBatch spriteBatch, GUICustomComponent mapContainer) + public void Draw(CampaignMode campaign, SpriteBatch spriteBatch, GUICustomComponent mapContainer) { tooltip = null; - var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + var currentDisplayLocation = campaign?.GetCurrentDisplayLocation(); Rectangle rect = mapContainer.Rect; @@ -501,13 +731,15 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + float missionIconScale = generationParams.MissionIcon != null ? 18.0f / generationParams.MissionIcon.SourceRect.Width : 1.0f; + Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - Vector2 topLeft = rectCenter + viewOffset; - Vector2 bottomRight = rectCenter + (viewOffset + new Vector2(Width, Height)); + Vector2 topLeft = rectCenter + viewOffset - rect.Location.ToVector2(); + Vector2 bottomRight = topLeft + new Vector2(Width, Height); Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1; @@ -568,7 +800,9 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { Location location = Locations[i]; - if (IsInFogOfWar(location)) { continue; } + if (!location.Discovered && IsInFogOfWar(location)) { continue; } + bool isEndLocation = endLocations.Contains(location); + if (!GameMain.DebugDraw && isEndLocation && location != endLocations.First()) { continue; } Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; @@ -577,24 +811,54 @@ namespace Barotrauma drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2; + if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableMissions.Any()) + { + Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y); + generationParams.MissionIcon.Draw(spriteBatch, + offScreenMissionIconPos, + generationParams.IndicatorColor, scale: missionIconScale * zoom); + GUI.Arrow.Draw(spriteBatch, + offScreenMissionIconPos + Vector2.UnitX * generationParams.MissionIcon.size.X * missionIconScale * zoom, + generationParams.IndicatorColor, MathHelper.PiOver2, scale: 0.5f); + } + + if (!rect.Intersects(drawRect)) { continue; } Color color = location.Type.SpriteColor; - if (!location.Discovered) { color = Color.White; } + if (!location.Visited) { color = Color.White; } if (location.Connections.Find(c => c.Locations.Contains(currentDisplayLocation)) == null) { color *= 0.5f; } float iconScale = location == currentDisplayLocation ? 1.2f : 1.0f; - if (location == HighlightedLocation) + if (location == HighlightedLocation) { iconScale *= 1.2f; } + if (isEndLocation) { iconScale *= 2.0f; } + + float notificationPulseAmount = 1.0f; + float notificationColorLerp = 0.0f; + if (mapNotifications.Any(n => n.RelatedLocation == location && n.IsCurrentlyVisible)) { - iconScale *= 1.2f; + float sin = MathF.Sin((float)Timing.TotalTime * 2.0f); + notificationPulseAmount = Math.Max(sin + 0.5f, 1.0f); + notificationColorLerp = (notificationPulseAmount - 1.0f) * 4.0f; + color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp); + iconScale *= notificationPulseAmount; } - locationSprite.Draw(spriteBatch, pos, color, + locationSprite.Draw(spriteBatch, pos, color, scale: generationParams.LocationIconSize / locationSprite.size.X * iconScale * zoom); + if (location.Faction != null) + { + float factionIconScale = iconScale * 0.7f; + Sprite factionIcon = location.Faction.Prefab.IconSmall ?? location.Faction.Prefab.Icon; + Color factionIconColor = Color.Lerp(color, location.Faction.Prefab.IconColor, notificationColorLerp); + factionIcon.Draw(spriteBatch, pos + new Vector2(-15, 15) * zoom, factionIconColor, + scale: generationParams.LocationIconSize / factionIcon.size.X * factionIconScale * zoom); + } + if (location == currentDisplayLocation) { if (SelectedLocation != null) @@ -626,7 +890,10 @@ namespace Barotrauma { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; - generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUIStyle.Red, scale: typeChangeIconScale * zoom); + Color iconColor = GUIStyle.Red; + color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp); + iconScale *= notificationPulseAmount; + generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, iconColor, scale: typeChangeIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { @@ -635,14 +902,17 @@ namespace Barotrauma } if (location != CurrentLocation && generationParams.MissionIcon != null) { - if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || location.AvailableMissions.Any(m => m.Prefab.Type == MissionType.GoTo)) + if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || + location.AvailableMissions.Any(m => m.Locations[0] == m.Locations[1])) { Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; - float missionIconScale = 18.0f / generationParams.MissionIcon.SourceRect.Width; generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos)) { - var availableMissions = CurrentLocation.AvailableMissions.Where(m => m.Locations.Contains(location)).Concat(location.AvailableMissions.Where(m => m.Prefab.Type == MissionType.GoTo)).Distinct(); + var availableMissions = CurrentLocation.AvailableMissions + .Where(m => m.Locations.Contains(location)) + .Concat(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])) + .Distinct(); tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); } } @@ -651,23 +921,19 @@ namespace Barotrauma if (GameMain.DebugDraw) { Vector2 dPos = pos; - if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) + if (location == HighlightedLocation) { + dPos.Y -= 80; + GUI.DrawString(spriteBatch, dPos + new Vector2(15, 32), "Faction: " + (location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + GUI.DrawString(spriteBatch, dPos + new Vector2(15, 50), "Secondary Faction: " + (location.SecondaryFaction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); dPos.Y += 48; - string name = $"Reputation: {location.Name}"; - Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); - dPos.Y += nameSize.Y + 16; - Rectangle bgRect = new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32); - bgRect.Inflate(8,8); - Color barColor = ToolBox.GradientLerp(location.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); - GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); - string reputationValue = ((int)location.Reputation.Value).ToString(); - Vector2 repValueSize = GUIStyle.SubHeadingFont.MeasureString(reputationValue); - GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); + if (PlayerInput.KeyDown(Keys.LeftShift)) + { + GUI.DrawString(spriteBatch, new Vector2(150,150), "Dist: " + + GetDistanceToClosestLocationOrConnection(CurrentLocation, int.MaxValue, loc => loc == location), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + + } } dPos.Y += 48; GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); @@ -684,97 +950,6 @@ namespace Barotrauma GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); drawRadiationTooltip = false; } - else if (HighlightedLocation != null) - { - drawRadiationTooltip = false; - Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; - pos.X += 50 * zoom; - pos.X = (int)pos.X; - pos.Y = (int)pos.Y; - Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); - Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); - Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y); - - int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); - List<(SubmarineClass subClass, int tier)> overrideTiers = null; - if (HighlightedLocation.CanHaveSubsForSale()) - { - overrideTiers = new List<(SubmarineClass subClass, int tier)>(); - foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) - { - if (subClass == SubmarineClass.Undefined) { continue; } - int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); - if (highestClassTier > 0 && highestClassTier > highestSubTier) - { - overrideTiers.Add((subClass, highestClassTier)); - } - } - } - int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0); - size.Y += subAvailabilityTextCount * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; - - bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; - LocalizedString repLabelText = null, repValueText = null; - Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; - if (showReputation) - { - repLabelText = TextManager.Get("reputation"); - repLabelSize = GUIStyle.Font.MeasureString(repLabelText); - repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); - size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; - repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); - size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); - } - - GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, - new Rectangle( - (int)(pos.X - 60 * GUI.Scale), - (int)(pos.Y - size.Y), - (int)(size.X + 120 * GUI.Scale), - (int)(size.Y * 2.2f)), - Color.Black * hudVisibility); - - var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); - topLeftPos += new Vector2(0.0f, nameSize.Y); - DrawText(HighlightedLocation.Type.Name); - if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) - { - topLeftPos += new Vector2(0.0f, descSize.Y); - DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); - } - - if (highestSubTier > 0) - { - DrawSubAvailabilityText("advancedsub.all", highestSubTier); - } - if (overrideTiers != null) - { - foreach (var (subClass, tier) in overrideTiers) - { - DrawSubAvailabilityText($"advancedsub.{subClass}", tier); - } - } - void DrawSubAvailabilityText(string tag, int tier) - { - topLeftPos += new Vector2(0.0f, typeSize.Y); - DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); - } - - if (showReputation) - { - topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - DrawText(repLabelText.Value); - topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); - Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); - RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); - } - - void DrawText(LocalizedString text, GUIFont font = null) => GUI.DrawString(spriteBatch, topLeftPos, text, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: font); - } if (drawRadiationTooltip) { @@ -892,7 +1067,7 @@ namespace Barotrauma } float a = 1.0f; - if (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered) + if (!connection.Locations[0].Visited && !connection.Locations[1].Visited) { if (IsInFogOfWar(connection.Locations[0])) { @@ -961,25 +1136,25 @@ namespace Barotrauma if (connection.Locked) { var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); if (unlockEvent != null) { Reputation unlockReputation = CurrentLocation.Reputation; Faction unlockFaction = null; - if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) + if (!unlockEvent.Faction.IsEmpty) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); unlockReputation = unlockFaction?.Reputation; } - - DrawIcon( - "LockedLocationConnection", (int)(28 * zoom), - RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", - ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true))))); + if (unlockReputation != null) + { + DrawIcon( + "LockedLocationConnection", (int)(28 * zoom), + RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", + ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true))))); + } } else { @@ -1042,13 +1217,14 @@ namespace Barotrauma private void DrawDecorativeHUD(SpriteBatch spriteBatch, Rectangle rect) { generationParams.DecorativeGraphSprite.Draw(spriteBatch, (int)((Timing.TotalTime * 5.0f) % generationParams.DecorativeGraphSprite.FrameCount), - new Vector2(rect.Left, rect.Top), Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale); + new Vector2(rect.X, rect.Bottom - (generationParams.DecorativeGraphSprite.FrameSize.Y + 30) * GUI.Scale), + Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale, SpriteEffects.FlipVertically); GUI.DrawString(spriteBatch, new Vector2(rect.Right - GUI.IntScale(170), rect.Y + GUI.IntScale(5)), "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, - new Vector2(rect.X + GUI.IntScale(15), rect.Bottom - GUI.IntScale(25)), + new Vector2(rect.X + GUI.IntScale(5), rect.Y + GUI.IntScale(5)), "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 51fa11094..151e15a14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -36,7 +36,7 @@ namespace Barotrauma public static List CopiedList = new List(); - private static List highlightedList = new List(); + private static List highlightedInEditorList = new List(); private static float highlightTimer; @@ -99,11 +99,24 @@ namespace Barotrauma { float depth = baseDepth //take texture into account to get entities with (roughly) the same base depth and texture to render consecutively to minimize texture swaps - + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.00001f - + ID % 100 * 0.000001f; + + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.000001f + + ID % 100 * 0.0000001f; return Math.Min(depth, 1.0f); } + protected Vector2 GetCollapseEffectOffset() + { + if (Level.Loaded?.Renderer?.CollapseEffectStrength is float collapseEffectStrength and > 0.0f && Submarine is not { Info.Type: SubmarineType.Player }) + { + Vector2 noisePos = new Vector2( + (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.5f) - 0.5f, + (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.1f) - 0.5f); + Vector2 offsetFromOrigin = Level.Loaded.Renderer.CollapseEffectOrigin - DrawPosition; + return offsetFromOrigin * MathF.Pow(collapseEffectStrength, MathHelper.Lerp(1, 4, ID % 1000 / 1000.0f)) + (noisePos * 100.0f * collapseEffectStrength); + } + return Vector2.Zero; + } + /// /// Update the selection logic in submarine editor /// @@ -118,10 +131,7 @@ namespace Barotrauma return; } - foreach (MapEntity e in mapEntityList) - { - e.isHighlighted = false; - } + ClearHighlightedEntities(); if (DisableSelect) { @@ -249,11 +259,10 @@ namespace Barotrauma if (i == 0) highLightedEntity = e; } } - UpdateHighlighting(highlightedEntities); } - if (highLightedEntity != null) highLightedEntity.isHighlighted = true; + if (highLightedEntity != null) { highLightedEntity.IsHighlighted = true; } } if (GUI.KeyboardDispatcher.Subscriber == null) @@ -275,7 +284,6 @@ namespace Barotrauma if (startMovingPos != Vector2.Zero) { Item targetContainer = GetPotentialContainer(position, SelectedList); - if (targetContainer != null) { targetContainer.IsHighlighted = true; } if (PlayerInput.PrimaryMouseButtonReleased()) @@ -597,10 +605,10 @@ namespace Barotrauma if (highlightedListBox != null) { if (GUI.MouseOn == highlightedListBox || highlightedListBox.IsParentOf(GUI.MouseOn)) return; - if (highlightedEntities.SequenceEqual(highlightedList)) return; + if (highlightedEntities.SequenceEqual(highlightedInEditorList)) return; } - highlightedList = highlightedEntities; + highlightedInEditorList = highlightedEntities; highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas) { @@ -1083,7 +1091,7 @@ namespace Barotrauma private void UpdateResizing(Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; @@ -1184,7 +1192,7 @@ namespace Barotrauma private void DrawResizing(SpriteBatch spriteBatch, Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b41a0f0a6..d82698f9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -55,21 +55,14 @@ namespace Barotrauma { if (!CastShadow) { return; } - if (convexHulls == null) + convexHulls ??= new List(); + var h = new ConvexHull( + new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), + IsHorizontal, + this) { - convexHulls = new List(); - } - - Vector2 halfSize = size / 2; - Vector2[] verts = new Vector2[] - { - position + new Vector2(-halfSize.X, halfSize.Y), - position + new Vector2(halfSize.X, halfSize.Y), - position + new Vector2(halfSize.X, -halfSize.Y), - position + new Vector2(-halfSize.X, -halfSize.Y), + IsExteriorWall = IsExteriorWall }; - - var h = new ConvexHull(verts, Color.Black, this); if (Math.Abs(rotation) > 0.001f) { h.Rotate(position, rotation); @@ -226,6 +219,9 @@ namespace Barotrauma min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y); max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y); } + Vector2 offset = GetCollapseEffectOffset(); + min += offset; + max += offset; if (min.X > worldView.Right || max.X < worldView.X) { return false; } if (min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } @@ -295,6 +291,7 @@ namespace Barotrauma if (isWiringMode) { color *= 0.15f; } Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; + drawOffset += GetCollapseEffectOffset(); float depth = GetDrawDepth(); @@ -504,7 +501,7 @@ namespace Barotrauma private bool ConditionalMatches(PropertyConditional conditional) { - if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) { return false; } @@ -533,7 +530,7 @@ namespace Barotrauma float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; if (!invalidMessage && i < Sections.Length) { - SetDamage(i, damage); + SetDamage(i, damage, isNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 06cfbc539..614365566 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -35,6 +35,13 @@ namespace Barotrauma Rectangle camView = cam.WorldView; camView = new Rectangle(camView.X - CullMargin, camView.Y + CullMargin, camView.Width + CullMargin * 2, camView.Height + CullMargin * 2); + if (Level.Loaded?.Renderer?.CollapseEffectStrength is > 0.0f) + { + //force everything to be visible when the collapse effect (which moves everything to a single point) is active + camView = Rectangle.Union(AbsRect(camView.Location.ToVector2(), camView.Size.ToVector2()), new Rectangle(Point.Zero, Level.Loaded.Size)); + camView.Y += camView.Height; + } + if (Math.Abs(camView.X - prevCullArea.X) < CullMoveThreshold && Math.Abs(camView.Y - prevCullArea.Y) < CullMoveThreshold && Math.Abs(camView.Right - prevCullArea.Right) < CullMoveThreshold && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index b3f830140..8491dd736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -116,7 +116,7 @@ namespace Barotrauma bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && - (PlayerInput.MidButtonHeld() || PlayerInput.LeftButtonHeld())) + (PlayerInput.MidButtonHeld() || PlayerInput.PrimaryMouseButtonHeld())) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; moveSpeed.X = -moveSpeed.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 1d862e8a6..cded39e96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -39,7 +39,7 @@ namespace Barotrauma { Color clr = CurrentHull == null ? Color.DodgerBlue : GUIStyle.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } - if (isObstructed) + if (!IsTraversable) { clr = Color.Black; } @@ -84,7 +84,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - (isObstructed ? Color.Gray : GUIStyle.Green) * 0.7f, width: 5, depth: 0.002f); + (IsTraversable ? GUIStyle.Green : Color.Gray) * 0.7f, width: 5, depth: 0.002f); } if (ConnectedGap != null) { @@ -123,6 +123,11 @@ namespace Barotrauma } } } + else if (spawnType == SpawnType.ExitPoint && ExitPointSize != Point.Zero) + { + GUI.DrawRectangle(spriteBatch, drawPos - ExitPointSize.ToVector2() / 2, ExitPointSize.ToVector2(), Color.Cyan, thickness: 5); + } + GUIStyle.SmallFont.DrawString(spriteBatch, ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), @@ -170,9 +175,9 @@ namespace Barotrauma if (PlayerInput.KeyDown(Keys.Space)) { - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in HighlightedEntities) { - if (!(e is WayPoint) || e == this || !e.IsHighlighted) { continue; } + if (e is not WayPoint || e == this) { continue; } if (linkedTo.Contains(e)) { @@ -251,6 +256,7 @@ namespace Barotrauma private bool ChangeSpawnType(GUIButton button, object obj) { + var prevSpawnType = spawnType; GUITextBlock spawnTypeText = button.Parent.GetChildByUserData("spawntypetext") as GUITextBlock; var values = (SpawnType[])Enum.GetValues(typeof(SpawnType)); int currIndex = values.IndexOf(spawnType); @@ -267,6 +273,7 @@ namespace Barotrauma } spawnType = values[currIndex]; spawnTypeText.Text = spawnType.ToString(); + if (spawnType == SpawnType.ExitPoint || prevSpawnType == SpawnType.ExitPoint) { CreateEditingHUD(); } return true; } @@ -412,6 +419,28 @@ namespace Barotrauma textBox.Text = string.Join(",", tags); textBox.Flash(GUIStyle.Green); }; + + if (SpawnType == SpawnType.ExitPoint) + { + var sizeField = GUI.CreatePointField(ExitPointSize, GUI.IntScale(20), TextManager.Get("dimensions"), paddedFrame.RectTransform); + GUINumberInput xField = null, yField = null; + foreach (GUIComponent child in sizeField.GetAllChildren()) + { + if (yField == null) + { + yField = child as GUINumberInput; + } + else + { + xField = child as GUINumberInput; + if (xField != null) { break; } + } + } + xField.MinValueInt = 0; + xField.OnValueChanged = (numberInput) => { ExitPointSize = new Point(numberInput.IntValue, ExitPointSize.Y); }; + yField.MinValueInt = 0; + yField.OnValueChanged = (numberInput) => { ExitPointSize = new Point(ExitPointSize.X, numberInput.IntValue); }; + } } editingHUD.RectTransform.Resize(new Point( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 460d06df1..6a5bfb3ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -75,7 +75,7 @@ namespace Barotrauma.Networking float gain = 1.0f; float noiseGain = 0.0f; Vector3? position = null; - if (character != null) + if (character != null && !character.IsDead) { if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1809817cb..f86c9cd48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -332,7 +332,6 @@ namespace Barotrauma.Networking FileSize = 0 }; - Md5Hash.Cache.Remove(directTransfer.FilePath); OnFinished(directTransfer); } break; @@ -414,7 +413,6 @@ namespace Barotrauma.Networking { finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); - Md5Hash.Cache.Remove(activeTransfer.FilePath); OnFinished(activeTransfer); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 34449232a..adf9fd222 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -76,6 +76,8 @@ namespace Barotrauma.Networking Interrupted } + private UInt16? debugStartGameCampaignSaveID; + private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted; public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize; @@ -326,7 +328,7 @@ namespace Barotrauma.Networking return serverEndpoint switch { LidgrenEndpoint lidgrenEndpoint => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), - SteamP2PEndpoint _ when ownerKey is Some { Value: var key } => new SteamP2POwnerPeer(callbacks, key), + SteamP2PEndpoint _ when ownerKey.TryUnwrap(out var key) => new SteamP2POwnerPeer(callbacks, key), SteamP2PEndpoint steamP2PServerEndpoint when ownerKey.IsNone() => new SteamP2PClientPeer(steamP2PServerEndpoint, callbacks), _ => throw new ArgumentOutOfRangeException() }; @@ -859,17 +861,24 @@ namespace Barotrauma.Networking ContentFile file = ContentPackageManager.EnabledPackages.All .Select(p => p.Files.FirstOrDefault(f => f.Path == filePath)) - .FirstOrDefault(f => !(f is null)); + .FirstOrDefault(f => f is not null); contentToPreload.AddIfNotNull(file); } + string campaignErrorInfo = string.Empty; + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + { + campaignErrorInfo = $" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}."; + } + GameMain.GameSession.EventManager.PreloadContent(contentToPreload); int subEqualityCheckValue = inc.ReadInt32(); if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0)) { - string errorMsg = "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server." + - " There may have been an error in receiving the up-to-date submarine file from the server."; + string errorMsg = + "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " + + $"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -886,7 +895,7 @@ namespace Barotrauma.Networking $"Mission equality check failed. Mission count doesn't match the server. " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + - $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -899,7 +908,7 @@ namespace Barotrauma.Networking $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + - $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -922,7 +931,7 @@ namespace Barotrauma.Networking ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; + ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -988,7 +997,7 @@ namespace Barotrauma.Networking GameMain.ModDownloadScreen.Reset(); ContentPackageManager.EnabledPackages.Restore(); - CampaignMode.StartRoundCancellationToken?.Cancel(); + GameMain.GameSession?.Campaign?.CancelStartRound(); if (SteamManager.IsInitialized) { @@ -1322,6 +1331,8 @@ namespace Barotrauma.Networking eventErrorWritten = false; GameMain.NetLobbyScreen.StopWaitingForStartRound(); + debugStartGameCampaignSaveID = null; + while (CoroutineManager.IsCoroutineRunning("EndGame")) { EndCinematic?.Stop(); @@ -1471,19 +1482,12 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } - else if (campaign.Map == null) - { - GameStarted = true; - DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); - GameMain.NetLobbyScreen.Select(); - roundInitStatus = RoundInitStatus.Interrupted; - yield return CoroutineStatus.Failure; - } - if (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) + if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) || + NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) { campaign.PendingSaveID = campaignSaveID; - DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0,0,60); + DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0, 0, 60); while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) { if (DateTime.Now > saveFileTimeOut) @@ -1494,16 +1498,27 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } - yield return new WaitForSeconds(0.1f); + yield return new WaitForSeconds(0.1f); } } + if (campaign.Map == null) + { + GameStarted = true; + DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); + GameMain.NetLobbyScreen.Select(); + roundInitStatus = RoundInitStatus.Interrupted; + yield return CoroutineStatus.Failure; + } + campaign.Map.SelectLocation(selectedLocationIndex); LevelData levelData = nextLocationIndex > -1 ? campaign.Map.Locations[nextLocationIndex].LevelData : campaign.Map.Connections[nextConnectionIndex].LevelData; + debugStartGameCampaignSaveID = campaign.LastSaveID; + if (roundSummary != null) { loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null); @@ -2587,31 +2602,24 @@ namespace Barotrauma.Networking public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { msg.WriteBoolean(characterInfo == null); + msg.WritePadBits(); if (characterInfo == null) { return; } - msg.WriteString(newName ?? string.Empty); + var head = characterInfo.Head; - msg.WriteByte((byte)characterInfo.Head.Preset.TagSet.Count); - foreach (Identifier tag in characterInfo.Head.Preset.TagSet) - { - msg.WriteIdentifier(tag); - } - msg.WriteByte((byte)characterInfo.Head.HairIndex); - msg.WriteByte((byte)characterInfo.Head.BeardIndex); - msg.WriteByte((byte)characterInfo.Head.MoustacheIndex); - msg.WriteByte((byte)characterInfo.Head.FaceAttachmentIndex); - msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); - msg.WriteColorR8G8B8(characterInfo.Head.HairColor); - msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor); + var netInfo = new NetCharacterInfo( + NewName: newName ?? string.Empty, + Tags: head.Preset.TagSet.ToImmutableArray(), + HairIndex: (byte)head.HairIndex, + BeardIndex: (byte)head.BeardIndex, + MoustacheIndex: (byte)head.MoustacheIndex, + FaceAttachmentIndex: (byte)head.FaceAttachmentIndex, + SkinColor: head.SkinColor, + HairColor: head.HairColor, + FacialHairColor: head.FacialHairColor, + JobVariants: GameMain.NetLobbyScreen.JobPreferences.Select(NetJobVariant.FromJobVariant).ToImmutableArray()); - var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; - int count = Math.Min(jobPreferences.Count, 3); - msg.WriteByte((byte)count); - for (int i = 0; i < count; i++) - { - msg.WriteIdentifier(jobPreferences[i].Prefab.Identifier); - msg.WriteByte((byte)jobPreferences[i].Variant); - } + msg.WriteNetSerializableStruct(netInfo); } public void Vote(VoteType voteType, object data) @@ -2623,7 +2631,13 @@ namespace Barotrauma.Networking using (var segmentTable = SegmentTableWriter.StartWriting(msg)) { segmentTable.StartNewSegment(ClientNetSegment.Vote); - Voting.ClientWrite(msg, voteType, data); + bool succeeded = Voting.ClientWrite(msg, voteType, data); + if (!succeeded) + { + throw new Exception( + $"Failed to write vote of type {voteType}: " + + $"data was of invalid type {data?.GetType().Name ?? "NULL"}"); + } } ClientPeer.Send(msg, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index c51b66457..3cff1e7b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -92,10 +92,10 @@ namespace Barotrauma.Networking Name = GameMain.Client.Name, OwnerKey = ownerKey, SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), - SteamAuthTicket = steamAuthTicket switch + SteamAuthTicket = steamAuthTicket?.Data switch { null => Option.None(), - var ticket => Option.Some(ticket.Data) + var ticketData => Option.Some(ticketData) }, GameVersion = GameMain.Version.ToString(), Language = GameSettings.CurrentConfig.Language.Value diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 54c4d7ca2..5c77d37c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Networking var packet = INetSerializableStruct.Read(inc); - packet.SteamAuthTicket.TryUnwrap(out byte[] ticket); + packet.SteamAuthTicket.TryUnwrap(out var ticket); Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index adbf863df..23a9f6bef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -1,5 +1,6 @@ #nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -69,6 +70,9 @@ namespace Barotrauma.Networking [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); @@ -281,7 +285,7 @@ namespace Barotrauma.Networking // ----------------------------------------------------------------------------- - float elementHeight = 0.075f; + const float elementHeight = 0.075f; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); @@ -294,6 +298,11 @@ namespace Barotrauma.Networking serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; + var languageLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("Language")); + new GUITextBlock(new RectTransform(Vector2.One, languageLabel.RectTransform), + ServerLanguageOptions.Options.FirstOrNull(o => o.Identifier == Language)?.Label ?? TextManager.Get("Unknown"), + textAlignment: Alignment.Right); + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), @@ -363,7 +372,7 @@ namespace Barotrauma.Networking packageText.Selected = true; } //workshop download link found - else if (package.Id is Some { Value: var ugcId } && ugcId is SteamWorkshopId) + else if (package.Id.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId) { packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name); } @@ -417,8 +426,9 @@ namespace Barotrauma.Networking GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } + Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; - ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); + ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray(); bool getBool(string key) { @@ -427,8 +437,34 @@ namespace Barotrauma.Networking } } - private static ContentPackageInfo[] ExtractContentPackageInfo(Func valueGetter) + private static ContentPackageInfo[] ExtractContentPackageInfo(string serverName, Func valueGetter) { + //workaround to ServerRules queries truncating the values to 255 bytes + int individualPackageIndex = 0; + string? individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); + if (!individualPackage.IsNullOrEmpty()) + { + List contentPackages = new List(); + do + { + string[] splitPackageInfo = individualPackage.Split(','); + if (splitPackageInfo.Length != 3) + { + DebugConsole.Log( + $"Error in a server's content package list: malformed content package info ({individualPackage})."); + return Array.Empty(); + } + string name = splitPackageInfo[0]; + string hash = splitPackageInfo[1]; + ulong.TryParse(splitPackageInfo[2], out ulong id); + contentPackages.Add(new ContentPackageInfo(name, hash, Option.Some(new SteamWorkshopId(id)))); + + individualPackageIndex++; + individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); + } while (!individualPackage.IsNullOrEmpty()); + return contentPackages.ToArray(); + } + string? joinedNames = valueGetter("contentpackage"); string? joinedHashes = valueGetter("contentpackagehash"); string? joinedWorkshopIds = valueGetter("contentpackageid"); @@ -438,9 +474,11 @@ namespace Barotrauma.Networking #warning TODO: genericize ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); - if (contentPackageNames.Length != contentPackageHashes.Length - || contentPackageHashes.Length != contentPackageIds.Length) + if (contentPackageNames.Length != contentPackageHashes.Length || contentPackageHashes.Length != contentPackageIds.Length) { + DebugConsole.Log( + $"The number of names, hashes and Workshop IDs on server \"{serverName}\"" + + $" doesn't match: {contentPackageNames.Length} names ({string.Join(", ", contentPackageNames)}), {contentPackageHashes.Length} hashes, {contentPackageIds.Length} ids)"); return Array.Empty(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs index ff9079caf..678d89cdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs @@ -35,7 +35,7 @@ namespace Barotrauma } private static Option InfoFromListEntry(Steamworks.Data.ServerInfo entry) => - entry.Name.IsNullOrEmpty() + entry.Name.IsNullOrEmpty() || entry.Address is null ? Option.None() : Option.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs index fe80749e5..caf2e0a20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs @@ -71,10 +71,10 @@ namespace Barotrauma foreach (var lobby in lobbies) { - string lobbyOwnerStr = lobby.GetData("lobbyowner"); + string lobbyOwnerStr = lobby.GetData("lobbyowner") ?? ""; lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); - string serverName = lobby.GetData("name"); + string serverName = lobby.GetData("name") ?? ""; if (string.IsNullOrEmpty(serverName)) { continue; } var ownerId = SteamId.Parse(lobbyOwnerStr); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index c6a97de44..2039ad51d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -3,11 +3,15 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Barotrauma.Steam; namespace Barotrauma.Networking { partial class ServerSettings : ISerializableEntity { + private static readonly LocalizedString packetAmountTooltip = TextManager.Get("ServerSettingsMaxPacketAmountTooltip"); + private static readonly RichString packetAmountTooltipWarning = RichString.Rich($"{packetAmountTooltip}\n\n‖color:gui.red‖{TextManager.Get("PacketLimitWarning")}‖end‖"); + partial class NetPropertyData { public GUIComponent GUIComponent; @@ -27,7 +31,15 @@ namespace Barotrauma.Networking if (GUIComponent == null) return null; else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected; else if (GUIComponent is GUITextBox textBox) return textBox.Text; - else if (GUIComponent is GUIScrollBar scrollBar) return scrollBar.BarScrollValue; + else if (GUIComponent is GUIScrollBar scrollBar) + { + if (property.PropertyType == typeof(int)) + { + return (int)MathF.Floor(scrollBar.BarScrollValue); + } + + return scrollBar.BarScrollValue; + } else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; else if (GUIComponent is GUINumberInput numInput) @@ -43,9 +55,9 @@ namespace Barotrauma.Networking else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; else if (GUIComponent is GUIScrollBar scrollBar) { - if (value.GetType() == typeof(int)) + if (value is int i) { - scrollBar.BarScrollValue = (int)value; + scrollBar.BarScrollValue = i; } else { @@ -78,7 +90,7 @@ namespace Barotrauma.Networking } } private Dictionary tempMonsterEnabled; - + partial void InitProjSpecific() { var properties = TypeDescriptor.GetProperties(GetType()).Cast(); @@ -367,6 +379,15 @@ namespace Barotrauma.Networking //*********************************************** + // Language + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); + var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDD.AddItem(language.Label, language.Identifier); + } + GetPropertyData(nameof(Language)).AssignGUIComponent(languageDD); + //changing server visibility on the fly is not supported in dedicated servers if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) { @@ -931,11 +952,58 @@ namespace Barotrauma.Networking return true; }; + + GUILayoutGroup karmaAndDosLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), antigriefingTab.RectTransform), isHorizontal: false); + GUILayoutGroup lowerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); + GUILayoutGroup upperLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); + // karma -------------------------------------------------------------------------- - var karmaBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsUseKarma")); + var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsUseKarma")); GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); + var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) + { + ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") + }; + GetPropertyData(nameof(EnableDoSProtection)).AssignGUIComponent(enableDosProtection); + + CreateLabeledSlider(lowerLayout, "ServerSettingsMaxPacketAmount", out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); + LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; + maxPacketSlider.Step = 0.001f; + maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); + maxPacketSlider.ToolTip = packetAmountTooltip; + maxPacketSlider.OnMoved = (scrollBar, _) => + { + GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; + int value = (int)MathF.Floor(scrollBar.BarScrollValue); + + LocalizedString valueText = value > PacketLimitMin + ? value.ToString() + : TextManager.Get("ServerSettingsNoLimit"); + + switch (value) + { + case <= PacketLimitMin: + textBlock.TextColor = GUIStyle.Green; + scrollBar.ToolTip = packetAmountTooltip; + break; + case < PacketLimitWarning: + textBlock.TextColor = GUIStyle.Red; + scrollBar.ToolTip = packetAmountTooltipWarning; + break; + default: + textBlock.TextColor = GUIStyle.TextColorNormal; + scrollBar.ToolTip = packetAmountTooltip; + break; + } + + textBlock.Text = $"{maxPacketCountLabel} {valueText}"; + return true; + }; + GetPropertyData(nameof(MaxPacketAmount)).AssignGUIComponent(maxPacketSlider); + maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); + karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index cde031084..765d1a5d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -276,6 +276,8 @@ namespace Barotrauma.Networking if (GameMain.Client?.Character != null) { var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; + if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; } + GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } //encode audio and enqueue it diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index bf262be14..28d0461ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -116,7 +116,9 @@ namespace Barotrauma.Networking bool spectating = Character.Controlled == null; float rangeMultiplier = spectating ? 2.0f : 1.0f; WifiComponent radio = null; - var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; + var messageType = + !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ? + ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; @@ -144,7 +146,7 @@ namespace Barotrauma.Networking if ((client.VoipSound.CurrentAmplitude * client.VoipSound.Gain * GameMain.SoundManager.GetCategoryGainMultiplier("voip")) > 0.1f) //TODO: might need to tweak { - if (client.Character != null && !client.Character.Removed) + if (client.Character != null && !client.Character.Removed && !client.Character.IsDead) { Vector3 clientPos = new Vector3(client.Character.WorldPosition.X, client.Character.WorldPosition.Y, 0.0f); Vector3 listenerPos = GameMain.SoundManager.ListenerPosition; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 67ba366d8..466b9418b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -13,13 +13,11 @@ namespace Barotrauma { public SubmarineInfo SubmarineInfo { get; set; } public bool TransferItems { get; set; } - public int DeliveryFee { get; set; } - public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems) { SubmarineInfo = submarineInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; } } @@ -128,64 +126,72 @@ namespace Barotrauma UpdateVoteTexts(connectedClients, VoteType.Sub); } - public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) + /// + /// Returns true if the given data is valid for the given vote type, + /// returns false otherwise. If it returns false, the message must + /// be discarded or reset by the caller, as it is now malformed :) + /// + public bool ClientWrite(IWriteMessage msg, VoteType voteType, object data) { msg.WriteByte((byte)voteType); switch (voteType) { case VoteType.Sub: - if (!(data is SubmarineInfo sub)) { return; } + if (data is not SubmarineInfo sub) { return false; } msg.WriteInt32(sub.EqualityCheckVal); - if (sub.EqualityCheckVal == 0) + if (sub.EqualityCheckVal <= 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for msg.WriteString(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: - if (!(data is GameModePreset gameMode)) { return; } + if (data is not GameModePreset gameMode) { return false; } msg.WriteIdentifier(gameMode.Identifier); break; case VoteType.EndRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool endRound) { return false; } + msg.WriteBoolean(endRound); break; case VoteType.Kick: - if (!(data is Client votedClient)) { return; } + if (data is not Client votedClient) { return false; } msg.WriteByte(votedClient.SessionId); break; case VoteType.StartRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool startRound) { return false; } + msg.WriteBoolean(startRound); break; case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is (SubmarineInfo voteSub, bool transferItems)) - { - //initiate sub vote - msg.WriteBoolean(true); - msg.WriteString(voteSub.Name); - msg.WriteBoolean(transferItems); - } - else + switch (data) { - // vote - if (!(data is int)) { return; } - msg.WriteBoolean(false); - msg.WriteInt32((int)data); + case (SubmarineInfo voteSub, bool transferItems): + //initiate sub vote + msg.WriteBoolean(true); + msg.WriteString(voteSub.Name); + msg.WriteBoolean(transferItems); + break; + case int vote: + // vote + msg.WriteBoolean(false); + msg.WriteInt32(vote); + break; + default: + return false; } break; case VoteType.TransferMoney: - if (!(data is int)) { return; } + if (data is not int money) { return false; } msg.WriteBoolean(false); //not initiating a vote - msg.WriteInt32((int)data); + msg.WriteInt32(money); break; } msg.WritePadBits(); + return true; } public void ClientRead(IReadMessage inc) @@ -322,33 +328,34 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName2 = inc.ReadString(); - var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); bool transferItems = inc.ReadBoolean(); - int deliveryFee = inc.ReadInt16(); - if (submarineInfo == null) + if (GameMain.GameSession != null) { - DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); - return; + var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); + if (submarineInfo == null) + { + DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); + return; + } + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems); } - submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems, deliveryFee); break; } - GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); - + GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); if (passed && submarineVoteInfo.SubmarineInfo is { } subInfo) { switch (voteType) { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 468f07e54..55d0d15cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -85,6 +85,9 @@ namespace Barotrauma.Particles [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")] public bool CopyEntityDir { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")] + public bool CopyTargetAngle { get; set; } + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } @@ -203,7 +206,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index dfdd7342a..9aa60c592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -75,21 +75,21 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } - if (particleCount >= MaxParticles) { for (int i = 0; i < particleCount; i++) { - if (particles[i].Prefab.Priority < prefab.Priority) + if (particles[i].Prefab.Priority < prefab.Priority || + (!particles[i].Prefab.DrawAlways && prefab.DrawAlways)) { RemoveParticle(i); break; } } - if (particleCount >= MaxParticles) { return null; } + if (particleCount >= MaxParticles) { return null; } } Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); @@ -109,26 +109,30 @@ namespace Barotrauma.Particles Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } - if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + if (!prefab.DrawAlways) + { + if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } + if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + } if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, prefab.DrawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; return particles[particleCount - 1]; } - public List GetPrefabList() + public static List GetPrefabList() { return ParticlePrefab.Prefabs.ToList(); } - public ParticlePrefab FindPrefab(string prefabName) + public static ParticlePrefab FindPrefab(string prefabName) { - return ParticlePrefab.Prefabs.Find(p => p.Identifier == prefabName); + ParticlePrefab.Prefabs.TryGet(prefabName, out ParticlePrefab prefab); + return prefab; } private void RemoveParticle(int index) @@ -170,7 +174,7 @@ namespace Barotrauma.Particles remove = true; } - if (remove) RemoveParticle(i); + if (remove) { RemoveParticle(i); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index e758564c5..0b7aa21af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -185,6 +185,9 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle be always rendered on top of entities?")] public bool DrawOnTop { get; private set; } + [Editable, Serialize(false, IsPropertySaveable.No, description: "Draw the particle even when it's calculated to be outside of view (the formula doesn't take scales into account). ")] + public bool DrawAlways { get; private set; } + [Editable, Serialize(ParticleBlendState.AlphaBlend, IsPropertySaveable.No, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index a0ac4f61a..8f61bf354 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -19,7 +19,7 @@ namespace Barotrauma public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool invert = false) { - if (!Enabled) return; + if (!Enabled) { return; } UpdateDrawPosition(); deformSprite?.Draw(cam, new Vector3(DrawPosition, MathHelper.Clamp(deformSprite.Sprite.Depth, 0, 1)), @@ -30,9 +30,9 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) { - if (!Enabled) return; + if (!Enabled) { return; } UpdateDrawPosition(); - if (sprite == null) return; + if (sprite == null) { return; } SpriteEffects spriteEffect = (Dir == 1.0f) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; if (mirrorX) { @@ -79,13 +79,13 @@ namespace Barotrauma new Vector2(DrawPosition.X, -DrawPosition.Y), Color.Cyan, 0, 5); } - if (bodyShapeTexture == null && IsValidShape(radius, height, width)) + if (bodyShapeTexture == null && IsValidShape(Radius, Height, Width)) { switch (BodyShape) { case Shape.Rectangle: { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(width), ConvertUnits.ToDisplayUnits(height)); + float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Width), ConvertUnits.ToDisplayUnits(Height)); if (maxSize > 128.0f) { bodyShapeTextureScale = 128.0f / maxSize; @@ -96,14 +96,14 @@ namespace Barotrauma } bodyShapeTexture = GUI.CreateRectangle( - (int)ConvertUnits.ToDisplayUnits(width * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(height * bodyShapeTextureScale)); + (int)ConvertUnits.ToDisplayUnits(Width * bodyShapeTextureScale), + (int)ConvertUnits.ToDisplayUnits(Height * bodyShapeTextureScale)); break; } case Shape.Capsule: case Shape.HorizontalCapsule: { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(radius), ConvertUnits.ToDisplayUnits(Math.Max(height, width))); + float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Radius), ConvertUnits.ToDisplayUnits(Math.Max(Height, Width))); if (maxSize > 128.0f) { bodyShapeTextureScale = 128.0f / maxSize; @@ -114,20 +114,20 @@ namespace Barotrauma } bodyShapeTexture = GUI.CreateCapsule( - (int)ConvertUnits.ToDisplayUnits(radius * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(Math.Max(height, width) * bodyShapeTextureScale)); + (int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale), + (int)ConvertUnits.ToDisplayUnits(Math.Max(Height, Width) * bodyShapeTextureScale)); break; } case Shape.Circle: - if (ConvertUnits.ToDisplayUnits(radius) > 128.0f) + if (ConvertUnits.ToDisplayUnits(Radius) > 128.0f) { - bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(radius); + bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(Radius); } else { bodyShapeTextureScale = 1.0f; } - bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(radius * bodyShapeTextureScale)); + bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale)); break; default: throw new NotImplementedException(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 23c90ff23..07ebfee4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -11,15 +11,13 @@ namespace Barotrauma public enum MouseButton { None = -1, - LeftMouse = 0, - RightMouse = 1, + PrimaryMouse = 0, + SecondaryMouse = 1, MiddleMouse = 2, MouseButton4 = 3, MouseButton5 = 4, MouseWheelUp = 5, - MouseWheelDown = 6, - PrimaryMouse, - SecondaryMouse + MouseWheelDown = 6 } public class KeyOrMouse @@ -65,10 +63,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonHeld(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonHeld(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonHeld(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonHeld(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonHeld(); case MouseButton.MouseButton4: @@ -95,10 +89,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonClicked(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonClicked(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonClicked(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonClicked(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonClicked(); case MouseButton.MouseButton4: @@ -218,11 +208,11 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.PrimaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); + return PlayerInput.PrimaryMouseLabel; case MouseButton.SecondaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); + return PlayerInput.SecondaryMouseLabel; default: - return TextManager.Get("input." + MouseButton.ToString().ToLowerInvariant()); + return TextManager.Get($"Input.{MouseButton}"); } } else @@ -270,6 +260,9 @@ namespace Barotrauma } #endif + public static readonly LocalizedString PrimaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Left" : "Right")}Mouse"); + public static readonly LocalizedString SecondaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Right" : "Left")}Mouse"); + public static Vector2 MousePosition { get { return new Vector2(mouseState.Position.X, mouseState.Position.Y); } @@ -317,120 +310,48 @@ namespace Barotrauma } public static bool PrimaryMouseButtonHeld() - { - if (MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool PrimaryMouseButtonDown() - { - if (MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool PrimaryMouseButtonReleased() - { - if (MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool PrimaryMouseButtonClicked() - { - if (MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool SecondaryMouseButtonHeld() - { - if (!MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool SecondaryMouseButtonDown() - { - if (!MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool SecondaryMouseButtonReleased() - { - if (!MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool SecondaryMouseButtonClicked() - { - if (!MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool LeftButtonHeld() { return AllowInput && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonDown() + public static bool PrimaryMouseButtonDown() { return AllowInput && oldMouseState.LeftButton == ButtonState.Released && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonReleased() + public static bool PrimaryMouseButtonReleased() { return AllowInput && mouseState.LeftButton == ButtonState.Released; } - public static bool LeftButtonClicked() + public static bool PrimaryMouseButtonClicked() { return (AllowInput && oldMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released); } - public static bool RightButtonHeld() + public static bool SecondaryMouseButtonHeld() { return AllowInput && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonDown() + public static bool SecondaryMouseButtonDown() { return AllowInput && oldMouseState.RightButton == ButtonState.Released && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonReleased() + public static bool SecondaryMouseButtonReleased() { return AllowInput && mouseState.RightButton == ButtonState.Released; } - public static bool RightButtonClicked() + public static bool SecondaryMouseButtonClicked() { return (AllowInput && oldMouseState.RightButton == ButtonState.Pressed diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 3da38f7df..1440b125b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -169,7 +169,10 @@ namespace Barotrauma sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + + (!ContentPackageManager.EnabledPackages.All.Any() ? + "None" : + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})")))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index 17e268d1e..fc8859439 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -7,17 +7,13 @@ namespace Barotrauma { class CampaignEndScreen : Screen { - private Video video; - private readonly CreditsPlayer creditsPlayer; private readonly Camera cam; public Action OnFinished; - private LocalizedString textOverlay; - private float textOverlayTimer; - private Vector2 textOverlaySize; + protected SlideshowPlayer slideshowPlayer; public CampaignEndScreen() { @@ -27,43 +23,45 @@ namespace Barotrauma ScrollBarEnabled = false, AllowMouseWheelScroll = false }; - new GUIButton(new RectTransform(new Vector2(0.1f), creditsPlayer.RectTransform, Anchor.BottomRight, maxSize: new Point(300, 50)) { AbsoluteOffset = new Point(GUI.IntScale(20)) }, - TextManager.Get("close")) + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => - { - creditsPlayer.Scroll = 1.0f; - return true; - } + creditsPlayer.Scroll = 1.0f; + return true; }; + cam = new Camera(); } public override void Select() { base.Select(); - - textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUIStyle.Font); - textOverlaySize = GUIStyle.Font.MeasureString(textOverlay); - textOverlayTimer = 0.0f; - - video = Video.Load(GameMain.GraphicsDeviceManager.GraphicsDevice, GameMain.SoundManager, "Content/SplashScreens/Ending.webm"); - video.Play(); + if (SlideshowPrefab.Prefabs.TryGet("campaignending".ToIdentifier(), out var slideshow)) + { + slideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); + } creditsPlayer.Restart(); creditsPlayer.Visible = false; - SteamAchievementManager.UnlockAchievement("campaigncompleted".ToIdentifier(), unlockClients: true); + UnlockAchievement("campaigncompleted"); + UnlockAchievement( + GameMain.GameSession is { Campaign.Settings.RadiationEnabled: true } ? + "campaigncompleted_radiationenabled" : + "campaigncompleted_radiationdisabled"); + + static void UnlockAchievement(string id) + { + SteamAchievementManager.UnlockAchievement(id.ToIdentifier(), unlockClients: true); + } } public override void Deselect() { - video?.Dispose(); - video = null; GUI.HideCursor = false; SoundPlayer.OverrideMusicType = Identifier.Empty; } public override void Update(double deltaTime) { + slideshowPlayer?.UpdateManually((float)deltaTime); if (creditsPlayer.Finished) { OnFinished?.Invoke(); @@ -73,46 +71,18 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - spriteBatch.Begin(); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); graphics.Clear(Color.Black); - if (video.IsPlaying) + SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); + if (slideshowPlayer != null && !slideshowPlayer.Finished) { - GUI.HideCursor = !GUI.PauseMenuOpen; - spriteBatch.Draw(video.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); + slideshowPlayer.DrawManually(spriteBatch); } else { - SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); - float duration = 20.0f; - float creditsDelay = 3.0f; - if (textOverlayTimer < duration + creditsDelay) - { - float textAlpha; - float fadeInTime = 5.0f, fadeOutTime = 3.0f; - textOverlayTimer += (float)deltaTime; - if (textOverlayTimer < fadeInTime) - { - textAlpha = textOverlayTimer / fadeInTime; - } - else if (textOverlayTimer > duration - fadeOutTime) - { - textAlpha = Math.Min((duration - textOverlayTimer) / fadeOutTime, 1.0f); - } - else - { - textAlpha = 1.0f; - } - GUIStyle.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); - } - else - { - GUI.HideCursor = false; - creditsPlayer.Visible = true; - } + GUI.HideCursor = false; + creditsPlayer.Visible = true; } - spriteBatch.End(); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(cam, spriteBatch); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 0bc8adcd3..bb8f98570 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -38,7 +38,6 @@ namespace Barotrauma protected set; } - public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } @@ -124,6 +123,7 @@ namespace Barotrauma public struct CampaignSettingElements { + public SettingValue SelectedPreset; public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; @@ -135,6 +135,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + PresetName = SelectedPreset.GetValue(), TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), @@ -185,9 +186,13 @@ namespace Barotrauma { const float verticalSize = 0.14f; + bool loadingPreset = false; + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); - GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length); + GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length + 1); + presetDropdown.AddItem(TextManager.Get("karmapreset.custom"), null); + presetDropdown.Select(0); presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); @@ -195,21 +200,30 @@ namespace Barotrauma { string name = settings.PresetName; presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + + if (settings.PresetName.Equals(prevSettings.PresetName, StringComparison.OrdinalIgnoreCase)) + { + presetDropdown.SelectItem(settings); + } } + var presetValue = new SettingValue( + get: () => presetDropdown.SelectedData is CampaignSettings settings ? settings.PresetName : string.Empty, + set: static _ => { }); // we do not need a way to set this value + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) { Spacing = GUI.IntScale(5) }; SettingValue tutorialEnabled = isSinglePlayer ? - CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) : - new SettingValue(() => false, b => { }); - SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize, OnValuesChanged) : + new SettingValue(static () => false, static _ => { }); + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize, OnValuesChanged); ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; - SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions); + SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions, OnValuesChanged); ImmutableArray> fundOptions = ImmutableArray.Create( new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), @@ -218,7 +232,7 @@ namespace Barotrauma ); SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; - SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions); + SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions, OnValuesChanged); ImmutableArray> difficultyOptions = ImmutableArray.Create( new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), @@ -228,30 +242,38 @@ namespace Barotrauma ); SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; - SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions); + SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions, OnValuesChanged); SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, - verticalSize); + verticalSize, + OnValuesChanged); - presetDropdown.OnSelected = (selected, o) => + presetDropdown.OnSelected = (_, o) => { - if (o is CampaignSettings settings) - { - tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); - radiationEnabled.SetValue(settings.RadiationEnabled); - maxMissionCountInput.SetValue(settings.MaxMissionCount); - startingFundsInput.SetValue(settings.StartingBalanceAmount); - difficultyInput.SetValue(settings.Difficulty); - startingSetInput.SetValue(settings.StartItemSet); - return true; - } - return false; + if (o is not CampaignSettings settings) { return false; } + + loadingPreset = true; + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + loadingPreset = false; + return true; }; + void OnValuesChanged() + { + if (loadingPreset) { return; } + presetDropdown.Select(0); + } + return new CampaignSettingElements { + SelectedPreset = presetValue, TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, @@ -261,7 +283,7 @@ namespace Barotrauma }; // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox - static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize) + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -286,9 +308,11 @@ namespace Barotrauma minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + numberInput.OnValueChanged += _ => onChanged(); + bool ChangeValue(GUIButton btn, object userData) { - if (!(userData is int change)) { return false; } + if (userData is not int change) { return false; } numberInput.IntValue += change; return true; @@ -298,7 +322,7 @@ namespace Barotrauma } static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, - ImmutableArray> options) + ImmutableArray> options, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -349,6 +373,8 @@ namespace Barotrauma return true; } + numberInput.OnValueChanged += _ => onChanged(); + void SetValue(int value) { numberInput.IntValue = value; @@ -358,7 +384,7 @@ namespace Barotrauma return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); } - static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize) + static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); @@ -370,6 +396,13 @@ namespace Barotrauma tickBox.Box.IgnoreLayoutGroups = true; tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + + tickBox.OnSelected += _ => + { + onChanged(); + return true; + }; + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index a3ae05b14..c23b2b190 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -194,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CurrentSettings.InitialMoney; + int initialMoney = CampaignSettings.CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -208,15 +208,15 @@ namespace Barotrauma { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(CurrentSettings, settings => + CreateCustomizeWindow(CampaignSettings.CurrentSettings, settings => { - CampaignSettings prevSettings = CurrentSettings; - CurrentSettings = settings; + CampaignSettings prevSettings = CampaignSettings.CurrentSettings; + CampaignSettings.CurrentSettings = settings; if (prevSettings.InitialMoney != settings.InitialMoney) { object selectedData = subList.SelectedData; UpdateSubList(SubmarineInfo.SavedSubmarines); - if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CurrentSettings.InitialMoney) + if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CampaignSettings.CurrentSettings.InitialMoney) { subList.Select(selectedData); } @@ -375,6 +375,7 @@ namespace Barotrauma { onClosed?.Invoke(elements.CreateSettings()); + GameSettings.SaveCurrentConfig(); return CampaignCustomizeSettings.Close(button, o); }; } @@ -399,7 +400,7 @@ namespace Barotrauma SubmarineInfo selectedSub = null; - if (!(subList.SelectedData is SubmarineInfo)) { return false; } + if (subList.SelectedData is not SubmarineInfo) { return false; } selectedSub = subList.SelectedData as SubmarineInfo; if (selectedSub.SubmarineClass == SubmarineClass.Undefined) @@ -419,7 +420,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = CurrentSettings; + CampaignSettings settings = CampaignSettings.CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -476,7 +477,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { return; } + if (child.UserData is not SubmarineInfo sub) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -487,9 +488,9 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - if (!(obj is SubmarineInfo sub)) { return true; } + if (obj is not SubmarineInfo sub) { return true; } #if !DEBUG - if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -551,7 +552,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignSettings.CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), @@ -563,7 +564,7 @@ namespace Barotrauma #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -573,7 +574,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignSettings.CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 00591d5b6..6cb2671ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -81,11 +81,21 @@ namespace Barotrauma tabs[(int)CampaignMode.InteractionType.Map] = CreateDefaultTabContainer(container, new Vector2(0.9f)); var mapFrame = new GUIFrame(new RectTransform(Vector2.One, GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); - new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + var mapContainer = new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + var notificationFrame = new GUIFrame(new RectTransform(new Point(mapContainer.Rect.Width, GUI.IntScale(40)), mapContainer.RectTransform, Anchor.BottomCenter), style: "ChatBox"); + new GUIFrame(new RectTransform(Vector2.One, mapFrame.RectTransform), style: "InnerGlow", color: Color.Black * 0.9f) { CanBeFocused = false + }; + + var notificationContainer = new GUICustomComponent(new RectTransform(new Vector2(0.98f, 1.0f), notificationFrame.RectTransform, Anchor.Center), DrawMapNotifications, null) + { + HideElementsOutsideFrame = true }; + var notificationHeader = new GUIImage(new RectTransform(new Vector2(0.1f, 1.0f), notificationFrame.RectTransform, Anchor.CenterLeft), style: "GUISlopedHeaderRight"); + var text = new GUITextBlock(new RectTransform(Vector2.One, notificationHeader.RectTransform, Anchor.Center), TextManager.Get("breakingnews"), font: GUIStyle.LargeFont); + notificationHeader.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.3f), 0); // crew tab ------------------------------------------------------------------------- @@ -152,18 +162,23 @@ namespace Barotrauma CreateUI(tabs[(int)CampaignMode.InteractionType.Map].Parent); } - GameMain.GameSession?.Map?.Draw(spriteBatch, mapContainer); + Campaign?.Map?.Draw(Campaign, spriteBatch, mapContainer); + } + + private void DrawMapNotifications(SpriteBatch spriteBatch, GUICustomComponent notificationContainer) + { + Campaign?.Map?.DrawNotifications(spriteBatch, notificationContainer); } private void UpdateMap(float deltaTime, GUICustomComponent mapContainer) { - var map = GameMain.GameSession?.Map; + var map = Campaign?.Map; if (map == null) { return; } - if (selectedLocation != null && selectedLocation == GameMain.GameSession.Campaign.GetCurrentDisplayLocation()) + if (selectedLocation != null && selectedLocation == Campaign.GetCurrentDisplayLocation()) { map.SelectLocation(-1); } - map.Update(deltaTime, mapContainer); + map.Update(Campaign, deltaTime, mapContainer); foreach (GUITickBox tickBox in missionTickBoxes) { bool disable = hasMaxMissions && !tickBox.Selected; @@ -260,14 +275,20 @@ namespace Barotrauma if (connection?.LevelData != null) { + if (location.Faction?.Prefab != null) + { + var factionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), + TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight, textColor: location.Faction.Prefab.IconColor); + } var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), connection.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)connection.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight); + if (connection.LevelData.HasBeaconStation) { var beaconStationContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -328,12 +349,31 @@ namespace Barotrauma if (connection != null && connection.Locations.Contains(currentDisplayLocation)) { List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); - if (!availableMissions.Contains(null)) { availableMissions.Insert(0, null); } + + if (!availableMissions.Any()) { availableMissions.Insert(0, null); } + + availableMissions.AddRange(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])); missionList.Content.ClearChildren(); + bool isPrevMissionInNextLocation = false; foreach (Mission mission in availableMissions) { + bool isMissionInNextLocation = mission != null && location.AvailableMissions.Contains(mission); + if (isMissionInNextLocation && !isPrevMissionInNextLocation) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionList.Content.RectTransform), TextManager.Get("outpostmissions"), + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + { + CanBeFocused = false + }; + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionList.Content.RectTransform), style: "HorizontalLine") + { + CanBeFocused = false + }; + } + isPrevMissionInNextLocation = isMissionInNextLocation; + var missionPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), missionList.Content.RectTransform), style: null) { UserData = mission @@ -347,45 +387,54 @@ namespace Barotrauma var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(15)); - if (mission != null) - { - var tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) + if (mission == null) + { + missionTextContent.RectTransform.MinSize = missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(35)); + missionTextContent.ChildAnchor = Anchor.CenterLeft; + } + else + { + GUITickBox tickBox = null; + if (!isMissionInNextLocation) { - UserData = mission, - Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false - }; - tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); - tickBox.RectTransform.IsFixedSize = true; - tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap); - tickBox.OnSelected += (GUITickBox tb) => - { - if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - - if (tb.Selected) + tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) { - Campaign.Map.CurrentLocation.SelectMission(mission); - } - else + UserData = mission, + Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false + }; + tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); + tickBox.RectTransform.IsFixedSize = true; + tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap); + tickBox.OnSelected += (GUITickBox tb) => { - Campaign.Map.CurrentLocation.DeselectMission(mission); - } + if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - foreach (GUITextBlock rewardText in missionRewardTexts) - { - Mission otherMission = rewardText.UserData as Mission; - rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); - } + if (tb.Selected) + { + Campaign.Map.CurrentLocation.SelectMission(mission); + } + else + { + Campaign.Map.CurrentLocation.DeselectMission(mission); + } - UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + foreach (GUITextBlock rewardText in missionRewardTexts) + { + Mission otherMission = rewardText.UserData as Mission; + rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); + } - if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) - { - GameMain.Client?.SendCampaignState(); - } - return true; - }; - missionTickBoxes.Add(tickBox); + UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) + { + GameMain.Client?.SendCampaignState(); + } + return true; + }; + missionTickBoxes.Add(tickBox); + } GUILayoutGroup difficultyIndicatorGroup = null; if (mission.Difficulty.HasValue) @@ -410,7 +459,7 @@ namespace Barotrauma float extraPadding = 0;// 0.8f * tickBox.Rect.Width; float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0; - missionName.Padding = new Vector4(missionName.Padding.X + tickBox.Rect.Width * 1.2f + extraPadding, + missionName.Padding = new Vector4(missionName.Padding.X + (tickBox?.Rect.Width ?? 0) * 1.2f + extraPadding, missionName.Padding.Y, missionName.Padding.Z + extraZPadding + extraPadding, missionName.Padding.W); @@ -425,9 +474,11 @@ namespace Barotrauma }; missionRewardTexts.Add(rewardText); - LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); - + LocalizedString reputationText = mission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); + } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.Description), wrap: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(0)); @@ -487,7 +538,7 @@ namespace Barotrauma OnClicked = (GUIButton btn, object obj) => { if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && - missionList.Content.Children.Any(c => c.UserData is Mission)) + missionList.Content.Children.Any(c => c.UserData is Mission mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation))) { var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => @@ -520,7 +571,7 @@ namespace Barotrauma //locationInfoPanel?.UpdateAuto(1.0f); } - public void SelectTab(CampaignMode.InteractionType tab, Identifier storeIdentifier = default) + public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) { if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) { @@ -541,7 +592,7 @@ namespace Barotrauma switch (selectedTab) { case CampaignMode.InteractionType.Store: - Store.SelectStore(storeIdentifier); + Store.SelectStore(npc); break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 102992cae..b3e163a16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -87,26 +87,26 @@ namespace Barotrauma.CharacterEditor private float spriteSheetZoom = 1; private float spriteSheetMinZoom = 0.25f; private float spriteSheetMaxZoom = 1; - private int spriteSheetOffsetY = 20; - private int spriteSheetOffsetX = 30; + private const int spriteSheetOffsetY = 20; + private const int spriteSheetOffsetX = 30; private bool hideBodySheet; private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 1.0f); private Vector2 cameraOffset; - private List selectedJoints = new List(); - private List selectedLimbs = new List(); - private HashSet editedCharacters = new HashSet(); + private readonly List selectedJoints = new List(); + private readonly List selectedLimbs = new List(); + private readonly HashSet editedCharacters = new HashSet(); private bool isEndlessRunner; private Rectangle spriteSheetRect; - private Rectangle CalculateSpritesheetRectangle() => + private Rectangle CalculateSpritesheetRectangle() => Textures == null || Textures.None() ? Rectangle.Empty : new Rectangle( - spriteSheetOffsetX, - spriteSheetOffsetY, - (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), + spriteSheetOffsetX, + spriteSheetOffsetY, + (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), (int)(Textures.Sum(t => t.Height) * spriteSheetZoom)); private const string screenTextTag = "CharacterEditor."; @@ -143,7 +143,7 @@ namespace Barotrauma.CharacterEditor var humanSpeciesName = CharacterPrefab.HumanSpeciesName; if (humanSpeciesName.IsEmpty) { - SpawnCharacter(AllSpecies.First()); + SpawnCharacter(VisibleSpecies.First()); } else { @@ -192,7 +192,7 @@ namespace Barotrauma.CharacterEditor jointEndLimb = null; anchor1Pos = null; jointStartLimb = null; - allSpecies = null; + visibleSpecies = null; onlyShowSourceRectForSelectedLimbs = false; unrestrictSpritesheet = false; editedCharacters.Clear(); @@ -214,15 +214,12 @@ namespace Barotrauma.CharacterEditor private void Reset(IEnumerable characters = null) { - if (characters == null) - { - characters = editedCharacters; - } + characters ??= editedCharacters; characters.ForEach(c => ResetParams(c)); ResetVariables(); } - private void ResetParams(Character character) + private static void ResetParams(Character character) { character.Params.Reset(true); foreach (var animation in character.AnimController.AllAnimParams) @@ -719,7 +716,7 @@ namespace Barotrauma.CharacterEditor cameraOffset = Vector2.Clamp(cameraOffset, min, max); } Cam.Position = targetPos + cameraOffset; - MapEntity.mapEntityList.ForEach(e => e.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); // Update widgets jointSelectionWidgets.Values.ForEach(w => w.Update((float)deltaTime)); limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); @@ -994,7 +991,7 @@ namespace Barotrauma.CharacterEditor var collider = character.AnimController.Collider; var colliderDrawPos = SimToScreen(collider.SimPosition); Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); - var endPos = SimToScreen(collider.SimPosition + forward * collider.radius); + var endPos = SimToScreen(collider.SimPosition + forward * collider.Radius); GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUIStyle.Green); GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + forward * 0.25f), Color.Blue); Vector2 left = forward.Left(); @@ -1363,7 +1360,7 @@ namespace Barotrauma.CharacterEditor private class WallGroup { public readonly List walls; - + public WallGroup(List walls) { this.walls = walls; @@ -1374,7 +1371,7 @@ namespace Barotrauma.CharacterEditor var clones = new List(); walls.ForEachMod(w => clones.Add(w.Clone() as Structure)); return new WallGroup(clones); - } + } } private void CloneWalls() @@ -1391,7 +1388,7 @@ namespace Barotrauma.CharacterEditor else if (i == 2) { clones[i].walls[j].Move(new Vector2(-originalWall.walls[j].Rect.Width, 0)); - } + } } } } @@ -1404,8 +1401,8 @@ namespace Barotrauma.CharacterEditor private WallGroup SelectLastClone(bool right) { - var lastWall = right - ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() + var lastWall = right + ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() : clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Left).First(); return clones.Where(c => c.walls.Contains(lastWall)).FirstOrDefault(); } @@ -1440,33 +1437,35 @@ namespace Barotrauma.CharacterEditor private Identifier currentCharacterIdentifier; private Identifier selectedJob = Identifier.Empty; - private List allSpecies; - private List AllSpecies + private List visibleSpecies; + private List VisibleSpecies { get { - if (allSpecies == null) - { -#if DEBUG - allSpecies = CharacterPrefab.Prefabs.Keys.OrderBy(p => p).ToList(); -#else - allSpecies = CharacterPrefab.Prefabs.Keys.Where(p => !p.Contains("variant")).OrderBy(p => p).ToList(); -#endif - allSpecies.ForEach(f => DebugConsole.NewMessage(f.Value, Color.White)); - } - return allSpecies; + visibleSpecies ??= CharacterPrefab.Prefabs.Where(ShowCreature).OrderBy(p => p.Identifier).Select(p => p.Identifier).ToList(); + return visibleSpecies; } } - private List vanillaCharacters; - private List VanillaCharacters + private bool ShowCreature(CharacterPrefab prefab) + { + Identifier speciesName = prefab.Identifier; + if (speciesName == CharacterPrefab.HumanSpeciesName) { return true; } + if (!VanillaCharacters.Contains(prefab.ContentFile)) + { + // Always show all custom characters. + return true; + } + if (CreatureMetrics.UnlockAll) { return true; } + return CreatureMetrics.Unlocked.Contains(speciesName); + } + + private IEnumerable vanillaCharacters; + private IEnumerable VanillaCharacters { get { - if (vanillaCharacters == null) - { - vanillaCharacters = GameMain.VanillaContent.GetFiles().ToList(); - } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); return vanillaCharacters; } } @@ -1475,7 +1474,7 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); IncreaseIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } @@ -1483,19 +1482,19 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); ReduceIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } private void GetCurrentCharacterIndex() { - characterIndex = AllSpecies.IndexOf(character.SpeciesName); + characterIndex = VisibleSpecies.IndexOf(character.SpeciesName); } private void IncreaseIndex() { characterIndex++; - if (characterIndex > AllSpecies.Count - 1) + if (characterIndex > VisibleSpecies.Count - 1) { characterIndex = 0; } @@ -1506,7 +1505,7 @@ namespace Barotrauma.CharacterEditor characterIndex--; if (characterIndex < 0) { - characterIndex = AllSpecies.Count - 1; + characterIndex = VisibleSpecies.Count - 1; } } @@ -1687,7 +1686,7 @@ namespace Barotrauma.CharacterEditor XElement overrideElement = null; if (duplicate != null) { - allSpecies = null; + visibleSpecies = null; if (!File.Exists(configFilePath)) { // If the file exists, we just want to overwrite it. @@ -1823,9 +1822,9 @@ namespace Barotrauma.CharacterEditor AnimationParams.Create(fullPath, name, animType, type); } } - if (!AllSpecies.Contains(name)) + if (!VisibleSpecies.Contains(name)) { - AllSpecies.Add(name); + VisibleSpecies.Add(name); } SpawnCharacter(name, ragdollParams); limbPairEditing = false; @@ -2678,23 +2677,33 @@ namespace Barotrauma.CharacterEditor { Stretch = true }; - // Character selection var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); - var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) { RelativeOffset = new Vector2(0, 0.2f) }, elementCount: 8, style: null); characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue); - foreach (var file in AllSpecies) + foreach (CharacterPrefab prefab in CharacterPrefab.Prefabs.OrderByDescending(p => p.Identifier)) { - characterDropDown.AddItem(file.Value.CapitaliseFirstInvariant(), file); + Identifier speciesName = prefab.Identifier; + if (ShowCreature(prefab)) + { + characterDropDown.AddItem(speciesName.Value.CapitaliseFirstInvariant(), speciesName).SetAsFirstChild(); + } + else if (!CreatureMetrics.Encountered.Contains(speciesName)) + { + // Using a matching placeholder string here ("hidden"). + var element = characterDropDown.AddItem(TextManager.Get("hiddensubmarines"), Identifier.Empty, textColor: Color.Gray * 0.75f); + element.SetAsLastChild(); + element.Enabled = false; + } } characterDropDown.SelectItem(currentCharacterIdentifier); characterDropDown.OnSelected = (component, data) => { Identifier characterIdentifier = (Identifier)data; + if (characterIdentifier.IsEmpty) { return true; } try { SpawnCharacter(characterIdentifier); @@ -2795,7 +2804,7 @@ namespace Barotrauma.CharacterEditor saveAllButton.OnClicked += (button, userData) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; @@ -2835,7 +2844,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -2973,7 +2982,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -3212,7 +3221,7 @@ namespace Barotrauma.CharacterEditor Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); } - #region ToggleButtons +#region ToggleButtons private enum Direction { Left, @@ -4235,7 +4244,7 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.radius) * 3 * Cam.Zoom; + Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; @@ -5008,9 +5017,9 @@ namespace Barotrauma.CharacterEditor // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic. float multiplier = 0.9f; l.body.SetSize(new Vector2(size.X, size.Y) * l.Scale * RagdollParams.TextureScale * multiplier); - TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.Radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.Width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.Height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); } private void RecalculateOrigin(Limb l, Vector2? newOrigin = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index e191d3e54..43968f02e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -1,6 +1,4 @@ using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; namespace Barotrauma { @@ -8,7 +6,7 @@ namespace Barotrauma { private GUIListBox listBox; - private ContentXElement configElement; + private readonly ContentXElement configElement; private float scrollSpeed; @@ -37,6 +35,8 @@ namespace Barotrauma set { listBox.BarScroll = value; } } + public readonly GUIButton CloseButton; + public CreditsPlayer(RectTransform rectT, string configFile) : base(null, rectT) { @@ -51,6 +51,10 @@ namespace Barotrauma configElement = doc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); Load(); + + CloseButton = new GUIButton(new RectTransform(new Vector2(0.1f), RectTransform, Anchor.BottomRight, maxSize: new Point(GUI.IntScale(300), GUI.IntScale(50))) + { AbsoluteOffset = new Point(GUI.IntScale(20), GUI.IntScale(20) + (Rect.Bottom - GameMain.GraphicsHeight)) }, + TextManager.Get("close")); } private void Load() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index ac0be6534..337cdb366 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Lights; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -14,9 +15,9 @@ namespace Barotrauma private RenderTarget2D renderTargetWater; private RenderTarget2D renderTargetFinal; - private Effect damageEffect; - private Texture2D damageStencil; - private Texture2D distortTexture; + public readonly Effect DamageEffect; + private readonly Texture2D damageStencil; + private readonly Texture2D distortTexture; private float fadeToBlackState; @@ -38,7 +39,7 @@ namespace Barotrauma }; //var blurEffect = LoadEffect("Effects/blurshader"); - damageEffect = EffectLoader.Load("Effects/damageshader"); + DamageEffect = EffectLoader.Load("Effects/damageshader"); PostProcessEffect = EffectLoader.Load("Effects/postprocess"); GradientEffect = EffectLoader.Load("Effects/gradientshader"); GrainEffect = EffectLoader.Load("Effects/grainshader"); @@ -46,9 +47,9 @@ namespace Barotrauma BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); - damageEffect.Parameters["xStencil"].SetValue(damageStencil); - damageEffect.Parameters["aMultiplier"].SetValue(50.0f); - damageEffect.Parameters["cMultiplier"].SetValue(200.0f); + DamageEffect.Parameters["xStencil"].SetValue(damageStencil); + DamageEffect.Parameters["aMultiplier"].SetValue(50.0f); + DamageEffect.Parameters["cMultiplier"].SetValue(200.0f); distortTexture = TextureLoader.FromFile("Content/Effects/distortnormals.png"); PostProcessEffect.Parameters["xDistortTexture"].SetValue(distortTexture); @@ -105,13 +106,13 @@ namespace Barotrauma c.DoVisibilityCheck(cam); if (c.IsVisible != wasVisible) { - c.AnimController.Limbs.ForEach(l => + foreach (var limb in c.AnimController.Limbs) { - if (l.LightSource != null) + if (limb.LightSource is LightSource light) { - l.LightSource.Enabled = c.IsVisible; + light.Enabled = c.IsVisible; } - }); + } } } @@ -187,6 +188,10 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:LOS", sw.ElapsedTicks); sw.Restart(); + + static bool IsFromOutpostDrawnBehindSubs(Entity e) + => e.Submarine is { Info.OutpostGenerationParams.DrawBehindSubs: true }; + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTarget); graphics.Clear(Color.Transparent); @@ -194,7 +199,7 @@ namespace Barotrauma //(= the background texture that's revealed when a wall is destroyed) into the background render target //These will be visible through the LOS effect. spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && !IsFromOutpostDrawnBehindSubs(e)); Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); @@ -221,7 +226,11 @@ namespace Barotrauma Level.Loaded.DrawBack(graphics, spriteBatch, cam); } - //draw alpha blended particles that are in water and behind subs + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && IsFromOutpostDrawnBehindSubs(e)); + spriteBatch.End(); + + //draw alpha blended particles that are in water and behind subs #if LINUX || OSX spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); #else @@ -336,12 +345,13 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); sw.Restart(); + DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"]; spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null, - damageEffect, + DamageEffect, cam.Transform); - Submarine.DrawDamageable(spriteBatch, damageEffect, false); + Submarine.DrawDamageable(spriteBatch, DamageEffect, false); spriteBatch.End(); sw.Stop(); @@ -368,7 +378,7 @@ namespace Barotrauma { graphics.DepthStencilState = DepthStencilState.None; graphics.SamplerStates[0] = SamplerState.LinearWrap; - graphics.BlendState = Lights.CustomBlendStates.Multiplicative; + graphics.BlendState = CustomBlendStates.Multiplicative; Quad.UseBasicEffect(GameMain.LightManager.LightMap); Quad.Render(); } @@ -399,12 +409,13 @@ namespace Barotrauma { GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; + GameMain.LightManager.LosEffect.Parameters["blurDistance"].SetValue(0.005f); GameMain.LightManager.LosEffect.Parameters["xTexture"].SetValue(renderTargetBackground); GameMain.LightManager.LosEffect.Parameters["xLosTexture"].SetValue(GameMain.LightManager.LosTexture); GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); Color losColor; - if (GameMain.LightManager.LosMode == LosMode.Transparent) + if (GameMain.LightManager.LosMode is LosMode.Transparent or LosMode.BlockOutsideView) { //convert the los color to HLS and make sure the luminance of the color is always the same //as the luminance of the ambient light color @@ -447,6 +458,11 @@ namespace Barotrauma Vector3 chromaticAberrationStrength = GameSettings.CurrentConfig.Graphics.ChromaticAberration ? new Vector3(-0.02f, -0.01f, 0.0f) : Vector3.Zero; + if (Level.Loaded?.Renderer != null) + { + chromaticAberrationStrength += new Vector3(-0.03f, -0.015f, 0.0f) * Level.Loaded.Renderer.ChromaticAberrationStrength; + } + if (Character.Controlled != null) { BlurStrength = Character.Controlled.BlurStrength * 0.005f; @@ -504,6 +520,11 @@ namespace Barotrauma spriteBatch.End(); } + if (GameMain.LightManager.DebugLos) + { + GameMain.LightManager.DebugDrawLos(spriteBatch, cam); + } + sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks); sw.Restart(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 4537be4ac..24e67f496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -219,7 +219,7 @@ namespace Barotrauma currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - var dummyLocations = GameSession.CreateDummyLocations(seed: currentLevelData.Seed); + var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); GameMain.LightManager.AddLight(pointerLightSource); @@ -343,7 +343,7 @@ namespace Barotrauma editorContainer.ClearChildren(); paramsList.Content.ClearChildren(); - foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) + foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Identifier.Value) @@ -359,7 +359,7 @@ namespace Barotrauma editorContainer.ClearChildren(); caveParamsList.Content.ClearChildren(); - foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) + foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -375,7 +375,7 @@ namespace Barotrauma editorContainer.ClearChildren(); ruinParamsList.Content.ClearChildren(); - foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams) + foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams.OrderBy(p => p.Identifier)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -391,7 +391,7 @@ namespace Barotrauma editorContainer.ClearChildren(); outpostParamsList.Content.ClearChildren(); - foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams) + foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 107c22358..9f1a60b02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,7 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private GUIDropDown serverExecutableDropdown; + private GUIDropDown languageDropdown, serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -82,6 +82,7 @@ namespace Barotrauma { GameMain.Instance.ResolutionChanged += () => { + SetMenuTabPositioning(); CreateHostServerFields(); CreateCampaignSetupUI(); SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); @@ -426,31 +427,33 @@ namespace Barotrauma var relativeSize = new Vector2(0.6f, 0.65f); var minSize = new Point(600, 400); var maxSize = new Point(2000, 1500); - var anchor = Anchor.CenterRight; - var pivot = Pivot.CenterRight; - Vector2 relativeSpacing = new Vector2(0.05f, 0.0f); - - menuTabs = new Dictionary(); + var anchor = Anchor.Center; + var pivot = Pivot.Center; + Vector2 relativeOffset = new Vector2(0.05f, 0.0f); - menuTabs[Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, - style: null); - menuTabs[Tab.Settings].CanBeFocused = false; - - menuTabs[Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); - menuTabs[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs = new Dictionary + { + [Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }, + style: null) + { + CanBeFocused = false + }, + [Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }), + [Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }) + }; CreateCampaignSetupUI(); var hostServerScale = new Vector2(0.7f, 1.2f); menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) - { RelativeOffset = relativeSpacing }); + { RelativeOffset = relativeOffset }); CreateHostServerFields(); //---------------------------------------------------------------------- - menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }); CreateTutorialTab(); this.game = game; @@ -466,6 +469,25 @@ namespace Barotrauma var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => + { + SelectTab(Tab.Empty); + return true; + }; + + SetMenuTabPositioning(); + } + + private void SetMenuTabPositioning() + { + foreach (GUIFrame menuTab in menuTabs.Values) + { + var anchor = GUI.IsUltrawide ? Anchor.Center : Anchor.CenterRight; + var pivot = GUI.IsUltrawide ? Pivot.Center : Pivot.CenterRight; + Vector2 relativeOffset = GUI.IsUltrawide ? Vector2.Zero : new Vector2(0.05f, 0.0f); + menuTab.RectTransform.SetPosition(anchor, pivot); + menuTab.RectTransform.RelativeOffset = relativeOffset; + } } private void CreateTutorialTab() @@ -893,12 +915,14 @@ namespace Barotrauma #endif } - string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + - " -public " + isPublicBox.Selected.ToString() + - " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + - " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + - " -karmaenabled " + (!karmaBox.Selected).ToString() + - " -maxplayers " + maxPlayersBox.Text; + string arguments = + "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + + " -public " + isPublicBox.Selected.ToString() + + " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + + " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + + " -karmaenabled " + (!karmaBox.Selected).ToString() + + " -maxplayers " + maxPlayersBox.Text + + $" -language \"{(LanguageIdentifier)languageDropdown.SelectedData}\""; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) { @@ -1039,22 +1063,29 @@ namespace Barotrauma #if UNSTABLE backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); #endif - backgroundSprite ??= (LocationType.Prefabs.Where(l => l.UseInMainMenu).GetRandomUnsynced())?.GetPortrait(0); - } - - if (backgroundSprite != null) - { - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, - aberrationStrength: 0.0f); + if (GUIStyle.GetComponentStyle("MainMenuBackground") is { } mainMenuStyle && + mainMenuStyle.Sprites.TryGetValue(GUIComponent.ComponentState.None, out var sprites)) + { + backgroundSprite = sprites.GetRandomUnsynced()?.Sprite; + } + backgroundSprite ??= LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(0); } var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + float vignetteScale = Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(vignette.size.X * vignetteScale / 2), 0, + (int)(GameMain.GraphicsWidth - vignette.size.X * vignetteScale / 2), GameMain.GraphicsHeight); + + if (backgroundSprite?.Texture != null) + { + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White, drawArea); + } + if (vignette != null) { - spriteBatch.Begin(blendState: BlendState.NonPremultiplied); - vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y)); - spriteBatch.End(); + vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, vignetteScale); } } @@ -1067,10 +1098,10 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - DrawBackground(graphics, spriteBatch); - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + DrawBackground(graphics, spriteBatch); + GUI.Draw(Cam, spriteBatch); if (selectedTab != Tab.Credits) @@ -1100,7 +1131,7 @@ namespace Barotrauma if (i == 0) { GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); - if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) + if (mouseOn && PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) { GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } @@ -1205,45 +1236,28 @@ namespace Barotrauma { menuTabs[Tab.HostServer].ClearChildren(); - string name = ""; - string password = ""; - int maxPlayers = 8; - bool isPublic = true; - bool banAfterWrongPassword = false; - bool karmaEnabled = true; - string selectedKarmaPreset = ""; - PlayStyle selectedPlayStyle = PlayStyle.Casual; - if (File.Exists(ServerSettings.SettingsFile)) + var serverSettings = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile, out _)?.Root ?? new XElement("serversettings"); + + var name = serverSettings.GetAttributeString("name", ""); + var password = serverSettings.GetAttributeString("password", ""); + var isPublic = serverSettings.GetAttributeBool("IsPublic", true); + var banAfterWrongPassword = serverSettings.GetAttributeBool("banafterwrongpassword", false); + + int maxPlayersElement = serverSettings.GetAttributeInt("maxplayers", 8); + if (maxPlayersElement > NetConfig.MaxPlayers) { - XDocument settingsDoc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); - if (settingsDoc != null) - { - name = settingsDoc.Root.GetAttributeString("name", name); - password = settingsDoc.Root.GetAttributeString("password", password); - isPublic = settingsDoc.Root.GetAttributeBool("public", isPublic); - banAfterWrongPassword = settingsDoc.Root.GetAttributeBool("banafterwrongpassword", banAfterWrongPassword); - - int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); - if (maxPlayersElement > NetConfig.MaxPlayers) - { - DebugConsole.IsOpen = true; - DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); - maxPlayersElement = NetConfig.MaxPlayers; - } - - maxPlayers = maxPlayersElement; - karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true); - selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default"); - string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual"); - Enum.TryParse(playStyleStr, out selectedPlayStyle); - } + DebugConsole.AddWarning($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead."); } + int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers); + + var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true); + var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual); Vector2 textLabelSize = new Vector2(1.0f, 0.05f); Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.04f); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.95f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.01f, Stretch = true @@ -1321,7 +1335,7 @@ namespace Barotrauma //other settings ----------------------------------------------------- //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), text: name, textAlignment: textAlignment) @@ -1373,6 +1387,21 @@ namespace Barotrauma }; label.RectTransform.IsFixedSize = true; + var languageLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("Language"), textAlignment: textAlignment); + languageDropdown = new GUIDropDown(new RectTransform(textFieldSize, languageLabel.RectTransform, Anchor.CenterRight)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(language.Label, language.Identifier); + } + var defaultLanguage = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + var settingsLanguage = serverSettings.GetAttributeIdentifier("language", defaultLanguage.Value).ToLanguageIdentifier(); + if (!ServerLanguageOptions.Options.Any(o => o.Identifier == settingsLanguage)) + { + settingsLanguage = defaultLanguage; + } + languageDropdown.Select(ServerLanguageOptions.Options.FindIndex(o => o.Identifier == settingsLanguage)); + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerExecutable"), textAlignment: textAlignment); const string vanillaServerOption = "Vanilla"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index df051fb9c..8536cf80a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -398,12 +398,9 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); //wtf - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 59514c304..74ad6abde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -301,7 +301,7 @@ namespace Barotrauma levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); - backgroundSprite = LocationType.Random(new MTRandom(intSeed))?.GetPortrait(intSeed); + backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); SeedBox.Text = levelSeed; } } @@ -1934,7 +1934,7 @@ namespace Barotrauma var selectedSub = component.UserData as SubmarineInfo; if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) { - if (selectedSub.Price > CampaignSetupUI.CurrentSettings.InitialMoney) + if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney) { new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive")); } @@ -2244,9 +2244,9 @@ namespace Barotrauma List rankOptions = new List(); foreach (PermissionPreset rank in PermissionPreset.List) { - rankOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => + rankOptions.Add(new ContextMenuOption(rank.DisplayName, isEnabled: true, onSelected: () => { - LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); + LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.DisplayName)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = delegate @@ -2350,7 +2350,7 @@ namespace Barotrauma }; foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - rankDropDown.AddItem(permissionPreset.Name, permissionPreset, permissionPreset.Description); + rankDropDown.AddItem(permissionPreset.DisplayName, permissionPreset, permissionPreset.Description); } rankDropDown.AddItem(TextManager.Get("CustomRank"), null); @@ -2758,12 +2758,10 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { + if (backgroundSprite?.Texture == null) { return; } graphics.Clear(Color.Black); - - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } @@ -3315,7 +3313,7 @@ namespace Barotrauma foreach (var subElement in SubList.Content.Children) { var sub = subElement.UserData as SubmarineInfo; - bool tooExpensive = sub.Price > CampaignSetupUI.CurrentSettings.InitialMoney; + bool tooExpensive = sub.Price > CampaignSettings.CurrentSettings.InitialMoney; if (tooExpensive || !sub.IsCampaignCompatible) { foreach (var textBlock in subElement.GetAllChildren()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index c48aa0ac5..56c74dcc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -166,7 +166,7 @@ namespace Barotrauma { prefabList.ClearChildren(); - var particlePrefabs = GameMain.ParticleManager.GetPrefabList(); + var particlePrefabs = ParticleManager.GetPrefabList(); foreach (ParticlePrefab particlePrefab in particlePrefabs) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), prefabList.Content.RectTransform) { MinSize = new Point(0, 20) }, @@ -204,7 +204,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var prefabList = GameMain.ParticleManager.GetPrefabList(); + var prefabList = ParticleManager.GetPrefabList(); foreach (ParticlePrefab prefab in prefabList) { foreach (XElement element in doc.Root.Elements()) @@ -273,7 +273,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var prefabList = GameMain.ParticleManager.GetPrefabList(); + var prefabList = ParticleManager.GetPrefabList(); foreach (ParticlePrefab otherPrefab in prefabList) { foreach (var subElement in doc.Root.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index f86553cb7..5d1831408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Xml.Linq; namespace Barotrauma @@ -241,6 +239,7 @@ namespace Barotrauma private GUITickBox filterPassword; private GUITickBox filterFull; private GUITickBox filterEmpty; + private GUIDropDown languageDropdown; private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; @@ -255,6 +254,7 @@ namespace Barotrauma private TernaryOption filterModdedValue = TernaryOption.Any; private ColumnLabel sortedBy; + private bool sortedAscending = true; private const float sidebarWidth = 0.2f; public ServerListScreen() @@ -425,10 +425,13 @@ namespace Barotrauma ternaryFilters = new Dictionary(); filterTickBoxes = new Dictionary(); + RectTransform createFilterRectT() + => new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform); + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { text ??= TextManager.Get(key); - var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + var tickBox = new GUITickBox(createFilterRectT(), text) { UserData = text, Selected = defaultState, @@ -450,6 +453,109 @@ namespace Barotrauma filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); + // Language filter + if (ServerLanguageOptions.Options.Any()) + { + var languageKey = "Language".ToIdentifier(); + var allLanguagesKey = "AllLanguages".ToIdentifier(); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get(languageKey), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + languageDropdown = new GUIDropDown(createFilterRectT(), selectMultiple: true); + + languageDropdown.AddItem(TextManager.Get(allLanguagesKey), allLanguagesKey); + var allTickbox = languageDropdown.ListBox.Content.FindChild(allLanguagesKey)?.GetChild(); + + // Spacer between "All" and the individual languages + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), languageDropdown.ListBox.Content.RectTransform) + { + MinSize = new Point(0, GUI.IntScaleCeiling(2)) + }, style: null) + { + Color = Color.DarkGray, + CanBeFocused = false + }; + + var selectedLanguages + = ServerListFilters.Instance.GetAttributeLanguageIdentifierArray( + languageKey, + Array.Empty()); + foreach (var (label, identifier, _) in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(label, identifier); + } + + if (!selectedLanguages.Any()) + { + selectedLanguages = ServerLanguageOptions.Options.Select(o => o.Identifier).ToArray(); + } + + foreach (var lang in selectedLanguages) + { + languageDropdown.SelectItem(lang); + } + + if (ServerLanguageOptions.Options.All(o => selectedLanguages.Any(l => o.Identifier == l))) + { + languageDropdown.SelectItem(allLanguagesKey); + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + + var langTickboxes = languageDropdown.ListBox.Content.Children + .Where(c => c.UserData is LanguageIdentifier) + .Select(c => c.GetChild()) + .ToArray(); + + bool inSelectedCall = false; + languageDropdown.OnSelected = (_, userData) => + { + if (inSelectedCall) { return true; } + try + { + inSelectedCall = true; + + if (Equals(allLanguagesKey, userData)) + { + foreach (var tb in langTickboxes) + { + tb.Selected = allTickbox.Selected; + } + } + + bool noneSelected = langTickboxes.All(tb => !tb.Selected); + bool allSelected = langTickboxes.All(tb => tb.Selected); + + if (allSelected != allTickbox.Selected) + { + allTickbox.Selected = allSelected; + } + + if (allSelected) + { + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + else if (noneSelected) + { + languageDropdown.Text = TextManager.Get("None"); + } + + var languages = languageDropdown.SelectedDataMultiple.OfType(); + + ServerListFilters.Instance.SetAttribute(languageKey, string.Join(", ", languages)); + GameSettings.SaveCurrentConfig(); + return true; + } + finally + { + inSelectedCall = false; + FilterServers(); + } + }; + } + // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { @@ -713,7 +819,7 @@ namespace Barotrauma private void SortList(ColumnLabel sortBy, bool toggle) { - if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } + if (labelHolder.GetChildByUserData(sortBy) is not GUIButton button) { return; } sortedBy = sortBy; @@ -730,51 +836,74 @@ namespace Barotrauma } } - bool ascending = arrowUp.Visible; + sortedAscending = arrowUp.Visible; if (toggle) { - ascending = !ascending; + sortedAscending = !sortedAscending; } - arrowUp.Visible = ascending; - arrowDown.Visible = !ascending; + arrowUp.Visible = sortedAscending; + arrowDown.Visible = !sortedAscending; serverList.Content.RectTransform.SortChildren((c1, c2) => { - if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } - if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } - - switch (sortBy) - { - case ColumnLabel.ServerListCompatible: - bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); - bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); - - if (s1Compatible == s2Compatible) { return 0; } - return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListHasPassword: - if (s1.HasPassword == s2.HasPassword) { return 0; } - return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListName: - // I think we actually want culture-specific sorting here? - return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); - case ColumnLabel.ServerListRoundStarted: - if (s1.GameStarted == s2.GameStarted) { return 0; } - return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPlayers: - return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPing: - return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch - { - (false, false) => 0, - (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), - (false, true) => 1, - (true, false) => -1 - }; - default: - return 0; - } + if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; } + if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; } + int comparison = sortedAscending ? 1 : -1; + return CompareServer(sortBy, s1, s2) * comparison; }); } + + private void InsertServer(ServerInfo serverInfo, GUIComponent component) + { + var children = serverList.Content.RectTransform.Children.Reverse().ToList(); + + int comparison = sortedAscending ? 1 : -1; + foreach (var child in children) + { + if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; } + if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0) + { + var index = serverList.Content.RectTransform.GetChildIndex(child); + component.RectTransform.RepositionChildInHierarchy(index + 1); + return; + } + } + component.RectTransform.SetAsFirstChild(); + } + + private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2) + { + switch (sortBy) + { + case ColumnLabel.ServerListCompatible: + bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); + bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); + + if (s1Compatible == s2Compatible) { return 0; } + return s1Compatible ? -1 : 1; + case ColumnLabel.ServerListHasPassword: + if (s1.HasPassword == s2.HasPassword) { return 0; } + return s1.HasPassword ? 1 : -1; + case ColumnLabel.ServerListName: + // I think we actually want culture-specific sorting here? + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture); + case ColumnLabel.ServerListRoundStarted: + if (s1.GameStarted == s2.GameStarted) { return 0; } + return s1.GameStarted ? 1 : -1; + case ColumnLabel.ServerListPlayers: + return s2.PlayerCount.CompareTo(s1.PlayerCount); + case ColumnLabel.ServerListPing: + return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch + { + (false, false) => 0, + (true, true) => s2Ping.CompareTo(s1Ping), + (false, true) => 1, + (true, false) => -1 + }; + default: + return 0; + } + } public override void Select() { @@ -821,6 +950,7 @@ namespace Barotrauma UpdateFriendsList(); panelAnimator?.Update(); + scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -840,7 +970,7 @@ namespace Barotrauma RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) { - if (!(child.UserData is ServerInfo serverInfo)) { continue; } + if (child.UserData is not ServerInfo serverInfo) { continue; } child.Visible = ShouldShowServer(serverInfo); } @@ -851,6 +981,20 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } + private bool AllLanguagesVisible + { + get + { + if (languageDropdown is null) { return true; } + + // CountChildren-1 because there's a separator element in there that can't be selected + int tickBoxCount = languageDropdown.ListBox.Content.CountChildren - 1; + int selectedCount = languageDropdown.SelectedIndexMultiple.Count(); + + return selectedCount >= tickBoxCount; + } + } + private bool ShouldShowServer(ServerInfo serverInfo) { #if !DEBUG @@ -918,6 +1062,14 @@ namespace Barotrauma } } + if (!AllLanguagesVisible) + { + if (!languageDropdown.SelectedDataMultiple.OfType().Contains(serverInfo.Language)) + { + return false; + } + } + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { var gameMode = (Identifier)tickBox.UserData; @@ -1031,8 +1183,8 @@ namespace Barotrauma if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } if (info.IsInServer - && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } - && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) + && info.ConnectCommand.TryUnwrap(out var command) + && command.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) { const int framePadding = 5; @@ -1270,7 +1422,7 @@ namespace Barotrauma serverPreview.Content.ClearChildren(); panelAnimator.RightEnabled = false; joinButton.Enabled = false; - selectedServer = null; + selectedServer = Option.None; if (selectedTab == TabEnum.All) { @@ -1370,8 +1522,7 @@ namespace Barotrauma UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } - SortList(sortedBy, toggle: false); - FilterServers(); + InsertServer(serverInfo, serverFrame); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1629,7 +1780,7 @@ namespace Barotrauma #endif } - private Color GetPingTextColor(int ping) + private static Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); @@ -1640,12 +1791,10 @@ namespace Barotrauma graphics.Clear(Color.CornflowerBlue); GameMain.TitleScreen.DrawLoadingText = false; - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); } @@ -1666,6 +1815,7 @@ namespace Barotrauma { ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } + GameSettings.SaveCurrentConfig(); } public void LoadServerFilters() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs new file mode 100644 index 000000000..458614977 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs @@ -0,0 +1,173 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; + +namespace Barotrauma +{ + class SlideshowPlayer : GUIComponent + { + private readonly SlideshowPrefab slideshowPrefab; + private readonly LocalizedString pressAnyKeyText; + + private int state; + + private Color overlayColor, textColor; + + private float timer; + + private LocalizedString currentText; + + public bool LastTextShown => state >= slideshowPrefab.Slides.Length; + public bool Finished => state > slideshowPrefab.Slides.Length; + + public SlideshowPlayer(RectTransform rectT, SlideshowPrefab prefab) : base(null, rectT) + { + slideshowPrefab = prefab; + overlayColor = Color.Black; + textColor = Color.Transparent; + pressAnyKeyText = TextManager.Get("pressanykey"); + RefreshText(); + } + + public void Restart() + { + state = 0; + } + + public void Finish() + { + state = slideshowPrefab.Slides.Length + 1; + } + + protected override void Update(float deltaTime) + { + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + if (!Visible || (Finished && timer > slide.FadeOutDuration)) { return; } + + timer += deltaTime; + + if (state == 0) + { + overlayColor = Color.Lerp(Color.Black, Color.White, Math.Min((timer - slide.FadeInDelay) / slide.FadeInDuration, 1.0f)); + } + else + { + overlayColor = Color.Lerp(Color.Transparent, Color.White, Math.Min((timer - slide.FadeInDelay) / slide.FadeInDuration, 1.0f)); + } + + if (timer > slide.TextFadeInDelay) + { + textColor = Color.Lerp(Color.Transparent, Color.White, Math.Min((timer - slide.TextFadeInDelay) / slide.TextFadeInDuration, 1.0f)); + if (AnyKeyHit()) + { + if (timer > slide.TextFadeInDelay + slide.FadeInDuration) + { + overlayColor = textColor = Color.Transparent; + timer = 0.0f; + state++; + RefreshText(); + } + else + { + timer = slide.TextFadeInDelay + slide.TextFadeInDuration; + } + } + } + else + { + textColor = Color.Transparent; + if (AnyKeyHit()) + { + timer = slide.TextFadeInDelay + slide.TextFadeInDuration; + } + } + + if (state >= slideshowPrefab.Slides.Length) + { + overlayColor = Color.Lerp(Color.White, Color.Transparent, Math.Min(timer / slide.FadeOutDuration, 1.0f)); + textColor = Color.Lerp(Color.White, Color.Transparent, Math.Min(timer / slide.FadeOutDuration, 1.0f)); + if (timer >= slide.FadeOutDuration) + { + state++; + RefreshText(); + } + } + + static bool AnyKeyHit() + { + return + PlayerInput.GetKeyboardState.GetPressedKeys().Any(k => PlayerInput.KeyHit(k)) || + PlayerInput.PrimaryMouseButtonClicked(); + } + } + + private void RefreshText() + { + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + currentText = slide.Text + .Replace("[submarine]", Submarine.MainSub?.Info.Name ?? GameMain.GameSession?.SubmarineInfo?.Name ?? "Unknown") + .Replace("[location]", Level.Loaded?.StartOutpost?.Info.Name ?? "Unknown"); + } + + protected override void Draw(SpriteBatch spriteBatch) + { + if (slideshowPrefab.Slides.IsEmpty) { return; } + + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + if ((Finished && timer > slide.FadeOutDuration)) { return; } + + var overlaySprite = slide.Portrait; + + if (overlaySprite != null) + { + Sprite prevPortrait = null; + if (state > 0 && state < slideshowPrefab.Slides.Length) + { + prevPortrait = slideshowPrefab.Slides[state - 1].Portrait; + DrawOverlay(prevPortrait, Color.White); + } + if (prevPortrait?.Texture != overlaySprite.Texture) + { + DrawOverlay(overlaySprite, overlayColor); + } + } + else + { + GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); + } + + if (!currentText.IsNullOrEmpty() && textColor.A > 0) + { + var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); + Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; + LocalizedString wrappedText = ToolBox.WrapText(currentText, GameMain.GraphicsWidth / 3, GUIStyle.Font); + Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); + Vector2 textPos = centerPos - textSize / 2; + backgroundSprite.Draw(spriteBatch, + centerPos, + Color.White * (textColor.A / 255.0f), + origin: backgroundSprite.size / 2, + rotate: 0.0f, + scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 2.0f)); + + GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (textColor.A / 255.0f)); + GUI.DrawString(spriteBatch, textPos, wrappedText, textColor); + + if (timer > slide.TextFadeInDelay * 2) + { + float alpha = Math.Min(timer - slide.TextFadeInDelay * 2, 1.0f); + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(pressAnyKeyText) / 2; + GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, pressAnyKeyText, Color.Black * (textColor.A / 255.0f) * alpha); + GUI.DrawString(spriteBatch, bottomTextPos, pressAnyKeyText, textColor * alpha); + } + } + + void DrawOverlay(Sprite sprite, Color color) + { + if (sprite.Texture == null) { return; } + GUI.DrawBackgroundSprite(spriteBatch, sprite, color); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 3e95da567..3ad662591 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1158,7 +1158,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in entityLists[categoryKey]) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } @@ -1177,7 +1177,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } @@ -1306,7 +1306,6 @@ namespace Barotrauma try { assemblyPrefab.Delete(); - UpdateEntityList(); OpenEntityMenu(MapEntityCategory.ItemAssembly); } catch (Exception e) @@ -2416,6 +2415,17 @@ namespace Barotrauma return true; } }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("beaconstationplacement")) + { + Selected = MainSub.Info.BeaconStationInfo is { Placement: Level.PlacementType.Top }, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.Placement = tb.Selected ? + Level.PlacementType.Top : + Level.PlacementType.Bottom; + return true; + } + }; beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); //------------------------------------------------------------------ @@ -3079,9 +3089,17 @@ namespace Barotrauma string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } - + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); - doc.SaveSafe(filePath); + try + { + doc.SaveSafe(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to save the item assembly to \"{filePath}\".", e); + return; + } var result = ContentPackageManager.ReloadContentPackage(existingContentPackage); if (!result.TryUnwrapSuccess(out var resultPackage)) @@ -3635,6 +3653,8 @@ namespace Barotrauma private void OpenEntityMenu(MapEntityCategory? entityCategory) { + UpdateEntityList(); + foreach (GUIButton categoryButton in entityCategoryButtons) { categoryButton.Selected = entityCategory.HasValue ? @@ -3762,14 +3782,14 @@ namespace Barotrauma { if (GUIContextMenu.CurrentContextMenu != null) { return; } - List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? - MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : + List targets = MapEntity.HighlightedEntities.Any(me => !MapEntity.SelectedList.Contains(me)) ? + MapEntity.HighlightedEntities.ToList() : new List(MapEntity.SelectedList); Item target = null; var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && ic is not Repairable && ic.GuiFrame != null)) { // Do not offer the ability to open the inventory if the inventory should never be drawn var container = item.GetComponent(); @@ -4014,7 +4034,7 @@ namespace Barotrauma pickerMutex = new object(), hexMutex = new object(); - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); + Vector2 relativeSize = new Vector2(0.4f * GUI.AspectRatioAdjustment, 0.3f); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) { @@ -4053,24 +4073,31 @@ namespace Barotrauma GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - colorPicker.RectTransform.RelativeSize.X, 1f), colorLayout.RectTransform), childAnchor: Anchor.TopRight); float currentHue = colorPicker.SelectedHue / 360f; - GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; - GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; - GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; - GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.15f }; + GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.1f + }; - new GUICustomComponent(new RectTransform(new Vector2(0.4f, 0.8f), colorInfoLayout.RectTransform), (batch, component) => + new GUICustomComponent(new RectTransform(Vector2.One, colorInfoLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), (batch, component) => { Rectangle rect = component.Rect; Point areaSize = new Point(rect.Width, rect.Height / 2); @@ -5236,15 +5263,15 @@ namespace Barotrauma } } - if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) + if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default) { if (dummyCharacter != null) { if (dummyCharacter.SelectedItem == null) { - foreach (var entity in MapEntity.mapEntityList) + foreach (var entity in MapEntity.HighlightedEntities) { - if (entity is Item item && entity.IsHighlighted && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { var container = item.GetComponents().ToList(); if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) @@ -5327,6 +5354,16 @@ namespace Barotrauma else { var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + foreach (var item in Item.ItemList) + { + //attached wires are not normally selectable (by clicking), + //but let's select them manually when selecting all + var wire = item.GetComponent(); + if (wire != null && wire.Connections.None(c => c == null) && !selectables.Contains(item)) + { + selectables.Add(item); + } + } lock (selectables) { selectables.ForEach(MapEntity.AddSelection); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index a5e37dcdb..7cf2b716b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -49,7 +49,7 @@ namespace Barotrauma } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "assistant")); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "captain")); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Info.GiveExperience(999999); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs index 48bde97dc..d96f57b72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs @@ -1,20 +1,18 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { - #warning TODO: implement properly public class ServerListFilters { private readonly Dictionary attributes = new Dictionary(); - private ServerListFilters() { } - - private ServerListFilters(XElement elem) + private ServerListFilters(XElement? elem) { - if (elem == null) { return; } + if (elem is null) { return; } foreach (var attr in elem.Attributes()) { attributes.Add(attr.NameAsIdentifier(), attr.Value); @@ -23,8 +21,6 @@ namespace Barotrauma public static void Init(XElement? elem) { - if (elem is null) { return; } - Instance = new ServerListFilters(elem); } @@ -50,17 +46,27 @@ namespace Barotrauma { if (attributes.TryGetValue(key, out string? val)) { - if (Enum.TryParse(val, out T result)) { return result; } + if (Enum.TryParse(val, ignoreCase: true, out T result)) { return result; } } return def; } + public LanguageIdentifier[] GetAttributeLanguageIdentifierArray(Identifier key, LanguageIdentifier[] def) + { + return attributes.TryGetValue(key, out string? val) + ? val.Split(",") + .Select(static s => s.Trim()) + .Where(static s => !s.IsNullOrWhiteSpace()) + .Select(static s => s.ToLanguageIdentifier()).ToArray() + : def; + } + public void SetAttribute(Identifier key, string val) { attributes[key] = val; } - public static ServerListFilters Instance { get; private set; } = new ServerListFilters(); + public static ServerListFilters Instance { get; private set; } = new ServerListFilters(null); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 64ba580ef..f53f49be0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -176,6 +176,10 @@ namespace Barotrauma void updateWaterAmbience(Sound sound, float volume) { SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); + if (Level.Loaded != null) + { + volume *= Level.Loaded.GenerationParams.WaterAmbienceVolume; + } if (chn is null || !chn.IsPlaying) { if (volume < 0.01f) { return; } @@ -513,6 +517,11 @@ namespace Barotrauma if (musicDisposed) { Thread.Sleep(60); } } + + public static void ForceMusicUpdate() + { + updateMusicTimer = 0.0f; + } private static void UpdateMusic(float deltaTime) { @@ -540,7 +549,7 @@ namespace Barotrauma IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); int mainTrackIndex = 0; - if (suitableMusic.Count() == 0) + if (suitableMusic.None()) { targetMusic[mainTrackIndex] = null; } @@ -564,11 +573,21 @@ namespace Barotrauma } } - if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection) + if (Level.Loaded != null && (Level.Loaded.Type == LevelData.LevelType.LocationConnection || Level.Loaded.GenerationParams.PlayNoiseLoopInOutpostLevel)) { + Identifier biome = Level.Loaded.LevelData.Biome.Identifier; + if (Level.Loaded.IsEndBiome && GameMain.GameSession?.Campaign is CampaignMode campaign) + { + //don't play end biome music in the path leading up to the end level(s) + if (!campaign.Map.EndLocations.Contains(Level.Loaded.StartLocation)) + { + biome = Level.Loaded.StartLocation.Biome.Identifier; + } + } + // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips(Level.Loaded.LevelData.Biome.Identifier, currentIntensity) : + GetSuitableMusicClips(biome, currentIntensity) : Enumerable.Empty(); if (suitableNoiseLoops.Count() == 0) { @@ -597,10 +616,17 @@ namespace Barotrauma targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } + IEnumerable suitableIntensityMusic = Enumerable.Empty(); + if (targetMusic[mainTrackIndex] is { MuteIntensityTracks: false } mainTrack && Screen.Selected == GameMain.GameScreen) + { + float intensity = currentIntensity; + if (mainTrack?.ForceIntensityTrack != null) + { + intensity = mainTrack.ForceIntensityTrack.Value; + } + suitableIntensityMusic = GetSuitableMusicClips("intensity".ToIdentifier(), intensity); + } //get the appropriate intensity layers for current situation - IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) : - Enumerable.Empty(); int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { @@ -729,6 +755,14 @@ namespace Barotrauma firstTimeInMainMenu = false; + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + var missionMusic = mission.GetOverrideMusicType(); + if (!missionMusic.IsEmpty) { return missionMusic; } + } + } if (Character.Controlled != null) { @@ -754,6 +788,11 @@ namespace Barotrauma } } + if (Level.Loaded is { IsEndBiome: true }) + { + return "endlevel".ToIdentifier(); + } + Submarine targetSubmarine = Character.Controlled?.Submarine; if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { @@ -765,8 +804,8 @@ namespace Barotrauma return "deep".ToIdentifier(); } - if (targetSubmarine != null) - { + if (targetSubmarine != null) + { float floodedArea = 0.0f; float totalArea = 0.0f; foreach (Hull hull in Hull.HullList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 14b0b68e2..bb3af5eee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -238,17 +238,24 @@ namespace Barotrauma public readonly float Volume; public readonly Vector2 IntensityRange; + public readonly bool MuteIntensityTracks; + public readonly float? ForceIntensityTrack; public readonly bool ContinueFromPreviousTime; public int PreviousTime; public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) { - Type = element.GetAttributeIdentifier("type", ""); - IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); - DuckVolume = element.GetAttributeBool("duckvolume", false); - this.Volume = element.GetAttributeFloat("volume", 1.0f); - ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); + Type = element.GetAttributeIdentifier(nameof(Type), ""); + IntensityRange = element.GetAttributeVector2(nameof(IntensityRange), new Vector2(0.0f, 100.0f)); + DuckVolume = element.GetAttributeBool(nameof(DuckVolume), false); + MuteIntensityTracks = element.GetAttributeBool(nameof(MuteIntensityTracks), false); + if (element.GetAttribute(nameof(ForceIntensityTrack)) != null) + { + ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); + } + Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); + ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs index 6b3d92896..d6ef57ebc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; - -namespace Barotrauma +namespace Barotrauma { partial class ConditionalSprite { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 765fa93ca..c1c8efe3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -144,13 +144,7 @@ namespace Barotrauma default: continue; } - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionalList.Add(new PropertyConditional(attribute)); - } - } + conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); } } @@ -188,7 +182,6 @@ namespace Barotrauma public float GetRotation(ref float rotationState, float randomRotationFactor) { - RotationSpeed = -Math.Abs(RotationSpeed); switch (RotationAnim) { case AnimationType.Sine: diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 9927c01b8..41be59e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -72,8 +72,9 @@ namespace Barotrauma float angle = 0.0f; float particleRotation = 0.0f; bool mirrorAngle = false; - if (emitter.Prefab.Properties.CopyEntityAngle) + if (emitter.Prefab.Properties.CopyEntityAngle || emitter.Prefab.Properties.CopyTargetAngle) { + bool entityAngleAssigned = false; Limb targetLimb = null; if (entity is Item item && item.body != null) { @@ -84,19 +85,23 @@ namespace Barotrauma particleRotation += MathHelper.Pi; mirrorAngle = true; } + entityAngleAssigned = true; } - else if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + if (emitter.Prefab.Properties.CopyTargetAngle || !entityAngleAssigned) { - targetLimb = c.AnimController.GetLimb(l); - } - else - { - for (int i = 0; i < targets.Count; i++) + if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { - if (targets[i] is Limb limb) + targetLimb = c.AnimController.GetLimb(l); + } + else + { + for (int i = 0; i < targets.Count; i++) { - targetLimb = limb; - break; + if (targets[i] is Limb limb) + { + targetLimb = limb; + break; + } } } } @@ -108,14 +113,19 @@ namespace Barotrauma particleRotation += offset; if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f) { - particleRotation += MathHelper.Pi; - mirrorAngle = true; + angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + particleRotation = -targetLimb.body.Rotation; + if (targetLimb.body.Dir < 0.0f) + { + particleRotation += MathHelper.Pi; + mirrorAngle = true; + } } } } emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation, mirrorAngle: mirrorAngle); - } + } } private bool ignoreMuffling; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 27c008170..264cf243f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Steam CanBeFocused = false }; var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform), - text: item.Title); + text: item.Title ?? ""); var itemDownloadProgress = new GUIProgressBar(new RectTransform((0.5f, 0.75f), itemFrame.RectTransform, Anchor.CenterRight), 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 258594f3d..831abe5b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -105,7 +105,8 @@ namespace Barotrauma.Steam currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.UgcId))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp + => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); @@ -117,6 +118,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); + currentLobby?.SetData("language", serverSettings.Language.ToString()); DebugConsole.Log("Lobby updated!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 26b32f26c..918437918 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -16,6 +16,9 @@ namespace Barotrauma.Steam private static readonly List initializationErrors = new List(); public static IReadOnlyList InitializationErrors => initializationErrors; + private static bool IsInitializedProjectSpecific + => Steamworks.SteamClient.IsValid && Steamworks.SteamClient.IsLoggedOn; + private static void InitializeProjectSpecific() { if (IsInitialized) { return; } @@ -23,7 +26,6 @@ namespace Barotrauma.Steam try { Steamworks.SteamClient.Init(AppID, false); - IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; if (IsInitialized) { @@ -43,13 +45,11 @@ namespace Barotrauma.Steam } catch (DllNotFoundException) { - IsInitialized = false; initializationErrors.Add("SteamDllNotFound".ToIdentifier()); } catch (Exception e) { DebugConsole.ThrowError("SteamManager initialization threw an exception", e); - IsInitialized = false; initializationErrors.Add("SteamClientInitFailed".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 124e77d1d..ed270c344 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -111,7 +111,7 @@ namespace Barotrauma.Steam { await Task.Yield(); - string thumbnailUrl = item.PreviewImageUrl; + string? thumbnailUrl = item.PreviewImageUrl; if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } var client = new RestClient(thumbnailUrl); var request = new RestRequest(".", Method.GET); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 415bda37b..779101b67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Steam private string ExtractTitle(ItemOrPackage itemOrPackage) => itemOrPackage.TryGet(out ContentPackage package) ? package.Name - : ((Steamworks.Ugc.Item)itemOrPackage).Title; + : (((Steamworks.Ugc.Item)itemOrPackage).Title ?? ""); private void CreateWorkshopItemDetailContainer( GUIFrame parent, @@ -340,6 +340,8 @@ namespace Barotrauma.Steam subscribeButton.OnClicked = (button, o) => { + if (!SteamManager.IsInitialized) { return false; } + if (!workshopItem.IsSubscribed) { workshopItem.Subscribe(); @@ -360,6 +362,8 @@ namespace Barotrauma.Steam new RectTransform(Vector2.Zero, subscribeButton.RectTransform), onUpdate: (deltaTime, component) => { + if (!SteamManager.IsInitialized) { return; } + if (subscribeButtonSprite.Style is { Identifier: { } styleId }) { if (workshopItem.IsSubscribed && styleId != minusButton) @@ -380,6 +384,8 @@ namespace Barotrauma.Steam new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center), onDraw: (spriteBatch, component) => { + if (!SteamManager.IsInitialized) { return; } + bool visible = workshopItem.IsSubscribed && (workshopItem.IsDownloading || workshopItem.IsDownloadPending @@ -407,6 +413,8 @@ namespace Barotrauma.Steam }, onUpdate: (deltaTime, component) => { + if (!SteamManager.IsInitialized) { return; } + displayedDownloadAmount = Math.Min( workshopItem.DownloadAmount, MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f)); @@ -450,7 +458,7 @@ namespace Barotrauma.Steam var title = new GUITextBlock( new RectTransform(Vector2.One, itemLayout.RectTransform), - workshopItem.Title, font: GUIStyle.Font) + workshopItem.Title ?? "", font: GUIStyle.Font) { CanBeFocused = false }; @@ -570,7 +578,7 @@ namespace Barotrauma.Steam var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform)); var selectedTitle = - new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title, + new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title ?? "", font: GUIStyle.LargeFont); var author = workshopItem.Owner; @@ -682,9 +690,9 @@ namespace Barotrauma.Steam TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) => { - authorButton.Text = author.Name; + authorButton.Text = author.Name ?? ""; authorButton.RectTransform.NonScaledSize = - ((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z), + ((int)(authorButton.Font.MeasureString(author.Name ?? "").X + authorPadding.X + authorPadding.Z), authorButton.RectTransform.NonScaledSize.Y); }); @@ -769,7 +777,7 @@ namespace Barotrauma.Steam var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); - CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + CreateTagsList((workshopItem.Tags ?? Array.Empty()).ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); #endregion var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index 7a3a9b85d..12b4b2876 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -113,7 +113,7 @@ namespace Barotrauma { case ModType.Workshop: pkgElem.SetAttributeValue("name", pkg.Name); - if (pkg.UgcId.TryUnwrap(out ContentPackageId ugcId)) + if (pkg.UgcId.TryUnwrap(out var ugcId)) { pkgElem.SetAttributeValue("id", ugcId.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 272f9e4c9..c5342a37c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -142,11 +142,17 @@ namespace Barotrauma GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); - Submarine.Draw(spriteBatch); - Submarine.DrawFront(spriteBatch); - Submarine.DrawDamageable(spriteBatch, null); - spriteBatch.End(); + DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); + DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f))); + DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true)); + DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true)); + + void DrawBatch(Action drawAction) + { + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); + drawAction.Invoke(); + spriteBatch.End(); + } GameMain.Instance.GraphicsDevice.SetRenderTarget(null); GameMain.Instance.GraphicsDevice.Viewport = prevViewport; diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb index 37e87a0173a8f7f5c73ad61cd294b75ae7814cff..4b95d883feb67d25dc3adad5278be84be922ef21 100644 GIT binary patch delta 717 zcmYLH&ubG=5dP-9-Q;B<7%C#fpo^4RZNU~rv_fr?n(Dz=x=PbyvBefb1BpL+GD|%b zK?th{MKFR?dK0{Q5f2JpdXkDKOa21MMK8~Jn|-)2JM-rI=9_u5`^|ehq-xdDLzjj# z9rgIia|PU(tJDyIwc&JGgyqv5_Xe>tJ$s?nIF8`j?7}N% z%mBE{wgfnpr#?rl%3E3FeS~7HJ%n7Gn@YSF`XLV(Kdg>P?r_p#iKm$Xei2Z%-b=82 z=*OU`&BBrSB!3mi{N|WPLM6wa()dTY=#0#^jgIYM<}jS8M90MEcm~_Y>e+s#cFP~! z!RBl&m*N7DY9@GP%Qva`ndVJj59X?MAUcrm4*&lZ6iKhusIRX?FPvl1Gv_65>a1HB iIU3ws-MF(}57w4fR-1uglpFO%6Xeb>w_20XX4ZeKk9Pq8 delta 164 zcmZ1?befAJ!q2IkmBE{JB1f~$9u@`$Uw5|%4#wq&f$TV4-X+9(47NNiAYvWDQ6x ME;eNN544*B0O(&OB>(^b diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb index f51787721cdf3843813eb6172ccb69f127305f90..5cffd1d1a6f5697268c8d718c6bff009fdf87944 100644 GIT binary patch delta 405 zcmX@l^+=E-!p|v%mGKeBM2=?uU=9WbUw5|%4hH?JXHu9Z&QewsC@79E&C5*7FUl>B zFH21}abRF!Wnf?sU|>{WU@~A}+PLo&vmhG-BO?PVP-(FtJ0nn*d*Xp?Mw7`pjM|e^ z7<(oQGaa%tP_R`1Dkw70D+dAvZ3SH*FWE@10tl>mxi~>`hI-`)RSH@l^~uJQ`Iw~{ z4JKM&xGZX+SKi&;i>DRG|q|0(1e?1f$7}EFwGxdKHyb z5Rv4`aV%mg20)dyyj;9oOhB(dyk&smLq-OU$un3gI2f6^it}?aQzp-4k)ABTs^rYX t8(fl_mz8*WRN(DTrfzZbAC=fk}6Qp0RXKZYu^9> delta 72 zcmV-O0Js0*5zh(;SWZG@1prS4kqCnizytsQO-Dvp2msJVJR1R#nj--LvEIr95Cs4M e0RRPXb6;~Y1_1y72b1Clc$0kyA^`!Dpa>kWq!LsB diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb index 7f09d5fe633c65a2edafa7e0eeb973a1442cc946..02d69944238fdc31774804ee34bcae17bac0813b 100644 GIT binary patch delta 842 zcmZuvJ!=$E6g~I7nasEfyBMp)Vo?wa5e*31Y|Lge3ZkI0xDg9Kq9DqO`5=%Y`@l{@ zNEj=z*c53}1yY7sSojAN`~!=XHiC`KcG$@2?cMh3#h;7?)~Lph z9l%!-0l@?AsaNeQABc6sWZ9f-{y1||UHu|+8wf4RUBK12C)4wCu4ACd;#joHdzalt za_m$yu+)00ZRMIF-}D+wsWlJ#Y>u7QS$eR?)#)K9lj}K&dtJpKrl;bLCkzQQP{1jj z9l$}}unQW{)CP!Y03XoD40QoA<-E=mkM+5m4ywNHBCignD))h~n;x9?10*-_cSM`|9m;sLbqON?> zuUpMA9u_BSJ{8)?i9-)Jx&k$BVGiz&UL2xdfF&+?G} zW}Nc<6Tol?`}_cEpqj3+;?(l=z8RO=UfURn__y#;g1u$?MI=mbgqKEPk3D|5G~al> Qu=uF` zhV;Uz8HekGr}NjtkCpo1{_PLX55K~I-8tk1!q8Zvz9E9slhsdp7Enczq4Pak;kTWp8?65HPc0z+YEsU;3q)d6`f~ROQ}K42dFjoxZ<3#H1j$U6xJ)Xmq>b~rl+bb z)S%rW_SrMKt#M+X^iJ(HZ2b*j><4D(M#jK;(dCG7P%=N5EHmRUNia+Uy}(i8DF`oB RdRy+hh?CBE%jvOK{|EBSK79ZH diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb index 1f290fc5c1ed740460bf482de9f1bb57fc3dbc8f..9951e2186b56992ef5c477c02a0f176eb5820b01 100644 GIT binary patch literal 2416 zcmbtWO>Y}j6ur-5dnOjeZY2<~fDvM8StONcimfjK{yQCqe7$CER+Y$d%;0CxLYWOy;0B`71rAApf%bGnx6-QyuT6l!gaV@ zIOhHBU@+{2z2&QgYlX$a;`JMOzc}uW#)DwF7mP=PW;gF|j(5AA)(63ZT6j0;E%(OV zu5hV8Jdus{a>G+==a0Wr(w7v~IZ14aQ$CsD(|M}*} zy_*W2v?X!AjqN!i|D{Fx(A^GuH-1lua7$u3{T^qDqU7;OsRa2gDok+>qsgaWrI-X!7nFk($ zO(sd*7I7W8J`tHl)G1NW9YZ(OIul2oL?E_t#C2fCiH%D@cMKilv@h~ynJaRne#D{@ z8rOl3HFk{sVG=yFCyiZW){Y6mSB{LVqLlSZqek)qvnD4EKlA`>@}^Axz-QX-T7Pg& z$mLl~*|o9c@gTe4q2DSno<~Fu<(+Aq22RglrR%-0d%)g|-34|n&S*Y`_$g|gM#P-8 z3z>Bjv#*r48%(#r}lxJ2{PGSphX(k)-sEzFxn;v=Q&- z#5vF9ShSXxAYAg!TW*r%}h!{^m36jKOl5hpRku(zYm)nYSfYV3=E%B9359 ze})gPke(a+n&-KS*i6+G#^09b zT~4lL7;C{{(5-g92rg19=Y=8u)3iF>`u4p}4+c}Q&iRzFdHA%^alJzj@;{9!kcz6| YG75Fc!sX_u(Z}9h>#-7)}mnPcK#SBQpk$H51hl(X2n?XIYRX^ovj@Pi}?uCtHGCZ)G z(?6-=c$~3?y2U3?2{(a?)J)h(BmYN zo;y|cknV$OsH8Ks>d+|7%v4IRC z9z^WW^SBdz;fdPk1i(=<&(z;d!+(Qrr<(1RZi$jE5uY`O=QwuCs^$^?N*Ug;%XqD@i(v z+30fm&z&299T|jC1~?tskA<4a%sS2gSj^l9It`@EqF@z;Mp0-Mg;r5$7llqy=oW<@ z2z$^C5pdjqW3Fz)yjeu7Rm56C8mmZS32Ck(%_U?_OIu4wdo@ZMkaRAgJskO5H4YF9 zv8U@Taf59&7s0(A)#OOQ86HSqs>KFPOj@wPz!tnN_-r7haxqh(s}VS5rUi=}nDpMi zg;(c3PKJkSB85wbwg_tb)yns3Wd~|kCY#OS=)hxyj}SjmKTNgS>9B`ssOMw6V{%H5W(X%3cimxAUAx7mc~NnvQly(A;F%$MmMPLs5vV#taL9d{ zBE1v=&l~|j_r7?T8`)tN9I7=F`w9~D}Pps3R=f}$Sxkxj)SQU}r1E>fYbP@%2dwYJx#gOgB@lAj>> z1KdPV+#LiR{5Se0N0M_(p3gULJ{F!zcJ1__X+U7j>|5`_1UNaW)@-QW{j^B=I<618 z?aN-&iR(A52CYCI0Eh!Q4+u<++56QZJ>MzLH+tQ;?{JBOo(5$VQfFUe%HDEI>?QX) zNt``rn^U1XLVW>`k>`#O6?%6pLEjy5Euwwo6Ah1tT0E3+AR?N*{yGozw?vw^p=q9l z^d!ssipR1v6lY1CP412`Qrdk4#VBG_NvoOoe45Yrl>#4|3eI?JT9DlUm3+1}RIP!l zlBfbXEk@x%?9*ChWz!-ado%(PuwP@jgjI=pA{7u8X(;5-8oD)2wN~HvyS|azyp3P(SZC#Y}KnNK45wSl<01S3$-?L(2 F{|~?T#h3s9 diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 5c27ba425..0e2ac8220 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index aec4b86f1..fd6943fa0 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx index 6f0781293..3ec964651 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx @@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture2D xStencil; sampler StencilSampler = sampler_state { Texture = ; }; +float4 solidColor; + float4 inColor; float aCutoff; @@ -16,7 +18,6 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; - float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord min(aDiff * aMultiplier, c.a)); } +float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; + float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); + + float aDiff = stencilColor.a - aCutoff; + + clip(aDiff); + + return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a)); +} + technique StencilShader { pass Pass1 @@ -37,3 +50,11 @@ technique StencilShader PixelShader = compile ps_4_0_level_9_1 main(); } } + +technique StencilShaderSolidColor +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 solidColorStencil(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx index 3a4242a3a..69370113c 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx @@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture xStencil; sampler StencilSampler = sampler_state { Texture = ; }; +float4 solidColor; + float4 inColor; float aCutoff; @@ -16,7 +18,6 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * inColor; - float4 stencilColor = tex2D(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord min(aDiff * aMultiplier, c.a)); } +float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * inColor; + float4 stencilColor = tex2D(StencilSampler, texCoord); + + float aDiff = stencilColor.a - aCutoff; + + clip(aDiff); + + return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a)); +} + technique StencilShader { pass Pass1 @@ -37,3 +50,11 @@ technique StencilShader PixelShader = compile ps_2_0 main(); } } + +technique StencilShaderSolidColor +{ + pass Pass1 + { + PixelShader = compile ps_2_0 solidColorStencil(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader.fx b/Barotrauma/BarotraumaClient/Shaders/losshader.fx index f761aa1bc..1e93bd9fb 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader.fx @@ -30,11 +30,18 @@ float xLosAlpha; float4 xColor; +float blurDistance; + float4 mainPS(VertexShaderOutput input) : COLOR0 { float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords); - float4 losColor = xLosTexture.Sample(LosSampler, input.TexCoords); - + + float4 losColor = xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); + losColor = losColor * 0.25f; + float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( @@ -53,4 +60,4 @@ technique LosShader VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 mainPS(); } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx index 4ea589cf4..6d08d4460 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx @@ -9,6 +9,8 @@ float aCutoff; float4x4 wearableUvToClipperUv; float clipperTexelSize; +float2 stencilUVmin, stencilUVmax; + float stencilSample(float2 texCoord, float2 offset) { return xStencil.Sample( @@ -20,6 +22,12 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord { float4 c = xTexture.Sample(TextureSampler, texCoord) * color; + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.y - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); + float minStencil = stencilSample(texCoord, float2(0,0)); minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx index 25dd7f3d3..d845e79d3 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx @@ -9,6 +9,8 @@ float aCutoff; float4x4 wearableUvToClipperUv; float clipperTexelSize; +float2 stencilUVmin, stencilUVmax; + float stencilSample(float2 texCoord, float2 offset) { return tex2D( @@ -19,6 +21,12 @@ float stencilSample(float2 texCoord, float2 offset) float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * color; + + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.y - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); float minStencil = stencilSample(texCoord, float2(0,0)); minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index adc6b7153..0e53d121f 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -14,7 +14,7 @@ Debug;Release;Unstable true app.manifest - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5ee5b2bc2..f271ea211 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6692e0d12..d884d6e0d 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index a8b32422a..0f0014017 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -71,6 +71,11 @@ namespace Barotrauma msg.WriteString(ragdollFileName); msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); + msg.WriteIdentifier(MinReputationToHire.factionId); + if (MinReputationToHire.factionId != default) + { + msg.WriteSingle(MinReputationToHire.reputation); + } if (Job != null) { msg.WriteUInt32(Job.Prefab.UintIdentifier); @@ -86,7 +91,7 @@ namespace Barotrauma msg.WriteByte((byte)0); } - msg.WriteUInt16((ushort)ExperiencePoints); + msg.WriteInt32(ExperiencePoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index b960f2f7d..5eff578d3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -692,6 +692,7 @@ namespace Barotrauma { msg.WriteIdentifier(MerchantIdentifier); } + msg.WriteIdentifier(Faction); int msgLengthBeforeOrders = msg.LengthBytes; // Current orders diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 9379178b2..a4c7e8509 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -13,6 +13,15 @@ namespace Barotrauma { static partial class DebugConsole { + private static readonly RateLimiter rateLimiter = new( + maxRequests: 10, + expiryInSeconds: 5, + punishmentRules: new[] + { + (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce), + (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick) + }); + public partial class Command { /// @@ -608,12 +617,12 @@ namespace Barotrauma NewMessage("Valid ranks are:", Color.White); foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - NewMessage(" - " + permissionPreset.Name, Color.White); + NewMessage(" - " + permissionPreset.DisplayName, Color.White); } ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) => { - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { ThrowError("Rank \"" + rank + "\" not found."); @@ -622,7 +631,7 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - NewMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); + NewMessage("Assigned the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White); }, args, 1); }); @@ -1401,6 +1410,44 @@ namespace Barotrauma })); + commands.Add(new Command("forcelocationtypechange", "", (string[] args) => + { + if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } + + if (args.Length < 2) + { + ThrowError("Invalid parameters. The command should be formatted as \"forcelocationtypechange [locationname] [locationtype]\". If the names consist of multiple words, you should surround them with quotation marks."); + return; + } + + var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (location == null) + { + ThrowError($"Could not find a location with the name {args[0]}."); + return; + } + + var locationType = LocationType.Prefabs.FirstOrDefault(lt => + lt.Name.Equals(args[1], StringComparison.OrdinalIgnoreCase) || lt.Identifier == args[1]); + if (location == null) + { + ThrowError($"Could not find the location type {args[1]}."); + return; + } + + location.ChangeType(GameMain.GameSession.Campaign, locationType); + }, + () => + { + if (GameMain.GameSession?.Campaign == null) { return null; } + + return new string[][] + { + GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), + LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() + }; + })); + AssignOnExecute("resetcharacternetstate", (string[] args) => { if (GameMain.Server == null) { return; } @@ -1672,13 +1719,7 @@ namespace Barotrauma "teleportsub", (Client client, Vector2 cursorWorldPos, string[] args) => { - if (Submarine.MainSub == null || Level.Loaded == null) return; - if (Level.Loaded.Type == LevelData.LevelType.Outpost) - { - GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client, Color.Red); - return; - } - + if (Submarine.MainSub == null || Level.Loaded == null) { return; } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(cursorWorldPos); @@ -1934,7 +1975,7 @@ namespace Barotrauma { GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); } - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); } ); @@ -1960,6 +2001,7 @@ namespace Barotrauma "freecam", (Client client, Vector2 cursorWorldPos, string[] args) => { + client.UsingFreeCam = true; GameMain.Server.SetClientCharacter(client, null); client.SpectateOnly = true; } @@ -2073,7 +2115,7 @@ namespace Barotrauma } string rank = string.Join("", args.Skip(1)); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red); @@ -2082,8 +2124,8 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient); - NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); + GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.DisplayName}\" to {client.Name}.", senderClient); + NewMessage(senderClient.Name + " granted the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White); } ); @@ -2477,7 +2519,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { item.TryCreateServerEventSpam(); - item.CreateStatusEvent(); + item.CreateStatusEvent(loadingRound: false); } foreach (Structure wall in Structure.WallList) { @@ -2497,6 +2539,17 @@ namespace Barotrauma #endif } + public static void ServerRead(IReadMessage inc, Client sender) + { + string consoleCommand = inc.ReadString(); + float cursorX = inc.ReadSingle(); + float cursorY = inc.ReadSingle(); + + if (rateLimiter.IsLimitReached(sender)) { return; } + + ExecuteClientCommand(sender, new Vector2(cursorX, cursorY), consoleCommand); + } + public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command) { if (GameMain.Server == null) return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index c4a043bbd..353ef08d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -41,7 +41,7 @@ namespace Barotrauma clientsToRemove.Add(k); } } - if (!(clientsToRemove is null)) + if (clientsToRemove is not null) { foreach (var k in clientsToRemove) { @@ -62,7 +62,7 @@ namespace Barotrauma { foreach (Entity e in targets) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -85,7 +85,7 @@ namespace Barotrauma IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -149,5 +149,15 @@ namespace Barotrauma } GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } + + public void ServerWriteSelectedOption(Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION_SELECTED_OPTION); + outmsg.WriteUInt16(Identifier); + outmsg.WriteByte((byte)(selectedOption + 1)); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs new file mode 100644 index 000000000..5369ec7e1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs @@ -0,0 +1,43 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class MissionAction : EventAction + { + private static readonly HashSet missionsUnlockedThisRound = new HashSet(); + + public static void ResetMissionsUnlockedThisRound() + { + missionsUnlockedThisRound.Clear(); + } + + public static void NotifyMissionsUnlockedThisRound(Client client) + { + foreach (Mission mission in missionsUnlockedThisRound) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission) + { + foreach (Client client in GameMain.Server.ConnectedClients) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission, Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); + outmsg.WriteIdentifier(mission.Prefab.Identifier); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[0]) ?? -1); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[1]) ?? -1); + outmsg.WriteString(mission.Name.Value); + GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 5d9dde87c..62b7f9882 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -14,12 +14,12 @@ namespace Barotrauma foreach (Event ev in activeEvents) { - if (!(ev is ScriptedEvent scriptedEvent)) { continue; } + if (ev is not ScriptedEvent scriptedEvent) { continue; } var actions = FindActions(scriptedEvent); foreach (EventAction action in actions.Select(a => a.Item2)) { - if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; } + if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; } if (!convAction.TargetClients.Contains(sender)) { #if DEBUG || UNSTABLE @@ -42,6 +42,14 @@ namespace Barotrauma else { convAction.SelectedOption = selectedOption; + if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption)) + { + foreach (Client c in convAction.TargetClients) + { + if (c == sender) { continue; } + convAction.ServerWriteSelectedOption(c); + } + } } } return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..6284dd232 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs @@ -0,0 +1,19 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + + boss.WriteSpawnData(msg, boss.ID, restrictMessageSize: false); + msg.WriteByte((byte)minions.Length); + foreach (Character minion in minions) + { + minion.WriteSpawnData(msg, minion.ID, restrictMessageSize: false); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index 65a5653e8..55e94e312 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -16,11 +17,10 @@ namespace Barotrauma foreach (var kvp in spawnedResources) { msg.WriteByte((byte)kvp.Value.Count); - var rotation = resourceClusters[kvp.Key].Rotation; - msg.WriteSingle(rotation); - foreach (var r in kvp.Value) + msg.WriteSingle(kvp.Value.FirstOrDefault()?.Rotation ?? 0.0f); + foreach (var item in kvp.Value) { - r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } } @@ -28,9 +28,9 @@ namespace Barotrauma { msg.WriteIdentifier(kvp.Key); msg.WriteByte((byte)kvp.Value.Length); - foreach (var i in kvp.Value) + foreach (var item in kvp.Value) { - msg.WriteUInt16(i.ID); + msg.WriteUInt16(item.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 52a944a4e..9653e372e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -6,33 +6,66 @@ namespace Barotrauma { partial class SalvageMission : Mission { - private bool usedExistingItem; + struct SpawnInfo + { + public readonly bool UsedExistingItem; + public readonly UInt16 OriginalInventoryID; + public readonly byte OriginalItemContainerIndex; + public readonly int OriginalSlotIndex; + public readonly List<(int listIndex, int effectIndex)> ExecutedEffectIndices; - private UInt16 originalInventoryID; - private byte originalItemContainerIndex; - private int originalSlotIndex; + public SpawnInfo(bool usedExistingItem, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex, List<(int listIndex, int effectIndex)> executedEffectIndices) + { + UsedExistingItem = usedExistingItem; + OriginalInventoryID = originalInventoryID; + OriginalItemContainerIndex = originalItemContainerIndex; + OriginalSlotIndex = originalSlotIndex; + ExecutedEffectIndices = executedEffectIndices; + } + } - private readonly List> executedEffectIndices = new List>(); + private readonly Dictionary spawnInfo = new Dictionary(); public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.WriteBoolean(usedExistingItem); - if (usedExistingItem) + foreach (var target in targets) { - msg.WriteUInt16(item.ID); - } - else - { - item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex); - } + bool targetFound = spawnInfo.ContainsKey(target) && target.Item != null; + msg.WriteBoolean(targetFound); + if (!targetFound) { continue; } - msg.WriteByte((byte)executedEffectIndices.Count); - foreach (Pair effectIndex in executedEffectIndices) + msg.WriteBoolean(spawnInfo[target].UsedExistingItem); + if (spawnInfo[target].UsedExistingItem) + { + msg.WriteUInt16(target.Item.ID); + } + else + { + target.Item.WriteSpawnData(msg, + target.Item.ID, + spawnInfo[target].OriginalInventoryID, + spawnInfo[target].OriginalItemContainerIndex, + spawnInfo[target].OriginalSlotIndex); + } + + msg.WriteByte((byte)spawnInfo[target].ExecutedEffectIndices.Count); + foreach ((int listIndex, int effectIndex) in spawnInfo[target].ExecutedEffectIndices) + { + msg.WriteByte((byte)listIndex); + msg.WriteByte((byte)effectIndex); + } + } + } + + public override void ServerWrite(IWriteMessage msg) + { + base.ServerWrite(msg); + msg.WriteByte((byte)targets.Count); + for (int i = 0; i < targets.Count; i++) { - msg.WriteByte((byte)effectIndex.First); - msg.WriteByte((byte)effectIndex.Second); + msg.WriteByte((byte)targets[i].State); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index ea3362d68..f41f62988 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -84,9 +84,6 @@ namespace Barotrauma Console.WriteLine("Loading game settings"); GameSettings.Init(); - Console.WriteLine("Loading MD5 hash cache"); - Md5Hash.Cache.Load(); - Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); @@ -182,7 +179,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-name": name = CommandLineArgs[i + 1]; @@ -248,7 +245,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-playstyle": Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle); @@ -270,6 +267,14 @@ namespace Barotrauma Server.ServerSettings.KarmaPreset = karmaPresetName; i++; break; + case "-language": + LanguageIdentifier language = CommandLineArgs[i + 1].ToLanguageIdentifier(); + if (ServerLanguageOptions.Options.Any(o => o.Identifier == language)) + { + Server.ServerSettings.Language = language; + } + i++; + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 82810135f..7629dd08f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -85,6 +84,7 @@ namespace Barotrauma { if (purchasedHullRepairs == value) { return; } purchasedHullRepairs = value; + PurchasedHullRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -95,6 +95,7 @@ namespace Barotrauma { if (purchasedLostShuttles == value) { return; } purchasedLostShuttles = value; + PurchasedLostShuttlesInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -105,6 +106,7 @@ namespace Barotrauma { if (purchasedItemRepairs == value) { return; } purchasedItemRepairs = value; + PurchasedItemRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -337,11 +339,12 @@ namespace Barotrauma IsFirstRound = true; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; } - Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); if (success) @@ -391,17 +394,14 @@ namespace Barotrauma NextLevel = newLevel; MirrorLevel = mirror; - //give clients time to play the end cinematic before starting the next round - if (transitionType == TransitionType.End) - { - yield return new WaitForSeconds(EndCinematicDuration); - } - else - { - yield return new WaitForSeconds(EndTransitionDuration * 0.5f); - } - GameMain.Server.TryStartGame(); + yield return new WaitForSeconds(EndTransitionDuration * 0.5f); + + //don't start the next round automatically if we just finished the campaign + if (transitionType != TransitionType.End) + { + GameMain.Server.TryStartGame(); + } yield return CoroutineStatus.Success; } @@ -424,7 +424,7 @@ namespace Barotrauma } public bool CanPurchaseSub(SubmarineInfo info, Client client) - => CanAfford(info.Price, client) && GetCampaignSubs().Contains(info); + => CanAfford(info.GetPrice(), client) && GetCampaignSubs().Contains(info); private readonly List discardedCharacters = new List(); public void DiscardClientCharacterData(Client client) @@ -493,7 +493,8 @@ namespace Barotrauma if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); - if (transitionType == TransitionType.End) + if (transitionType == TransitionType.End || + (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation)) { LoadNewLevel(); } @@ -509,6 +510,14 @@ namespace Barotrauma } } } + else if (Level.Loaded.IsEndBiome) + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.ProgressToNextLocation) + { + LoadNewLevel(); + } + } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { KeepCharactersCloseToOutpost(deltaTime); @@ -704,10 +713,6 @@ namespace Barotrauma if (requiredFlags.HasFlag(NetFlags.Reputation)) { msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Reputation)); - Reputation reputation = Map?.CurrentLocation?.Reputation; - msg.WriteBoolean(reputation != null); - if (reputation != null) { msg.WriteSingle(reputation.Value); } - // hopefully we'll never have more than 128 factions msg.WriteByte((byte)Factions.Count); foreach (Faction faction in Factions) @@ -823,54 +828,37 @@ namespace Barotrauma Bank.ForceUpdate(); } - if (purchasedHullRepairs != PurchasedHullRepairs) + if (purchasedHullRepairs && !PurchasedHullRepairs) { - switch (purchasedHullRepairs) + if (GetBalance(sender) >= hullRepairCost) { - case true when GetBalance(sender) >= hullRepairCost: - TryPurchase(sender, hullRepairCost); - PurchasedHullRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - break; - case false: - PurchasedHullRepairs = false; - personalWallet.Refund(hullRepairCost); - break; + TryPurchase(sender, hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); } } - if (purchasedItemRepairs != PurchasedItemRepairs) + if (purchasedItemRepairs && !PurchasedItemRepairs) { - switch (purchasedItemRepairs) + if (GetBalance(sender) >= itemRepairCost) { - case true when GetBalance(sender) >= itemRepairCost: - TryPurchase(sender, itemRepairCost); - PurchasedItemRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - break; - case false: - PurchasedItemRepairs = false; - personalWallet.Refund(itemRepairCost); - break; + TryPurchase(sender, itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); } } - if (purchasedLostShuttles != PurchasedLostShuttles) + if (purchasedLostShuttles && !PurchasedLostShuttles) { if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && TryPurchase(sender, shuttleRetrieveCost)) + else if (TryPurchase(sender, shuttleRetrieveCost)) { PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } - else if (!purchasedItemRepairs) - { - PurchasedLostShuttles = false; - personalWallet.Refund(shuttleRetrieveCost); - } } if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) @@ -1021,12 +1009,13 @@ namespace Barotrauma bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } + var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); foreach (var (prefab, category, _) in purchasedUpgrades) { UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging - int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); int level = UpgradeManager.GetUpgradeLevel(prefab, category); GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); } @@ -1057,49 +1046,47 @@ namespace Barotrauma if (GameMain.Server is null) { return; } - switch (transfer.Sender) + if (transfer.Sender.TryUnwrap(out var id)) { - case Some { Value: var id }: - if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } + if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - TransferMoney(wallet); - break; - case None _: - if (!AllowedToManageWallets(sender)) + TransferMoney(wallet); + } + else + { + if (!AllowedToManageWallets(sender)) + { + if (transfer.Receiver.TryUnwrap(out var receiverId) && receiverId == sender.CharacterID) { - if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) - { - if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } - GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); - GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); - } - return; + if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } + GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); + GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); } + return; + } - TransferMoney(Bank); - break; + TransferMoney(Bank); } void TransferMoney(Wallet from) { if (!from.TryDeduct(transfer.Amount)) { return; } - switch (transfer.Receiver) + if (transfer.Receiver.TryUnwrap(out var id)) { - case Some { Value: var id }: - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - wallet.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; - case None _: - Bank.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; + wallet.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); + } + else + { + Bank.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); } } @@ -1322,6 +1309,10 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { + //disconnected clients can never purchase anything + //(can happen e.g. if someone starts a vote to buy something and then disconnects) + if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; } + Wallet wallet = GetWallet(client); if (!AllowedToManageWallets(client)) { @@ -1371,6 +1362,12 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); modeElement.Add(Bank.Save()); + + if (GameMain.GameSession?.EventManager != null) + { + modeElement.Add(GameMain.GameSession?.EventManager.Save()); + } + CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e46134ca6..d2a706459 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -11,20 +11,15 @@ namespace Barotrauma { internal partial class MedicalClinic { - private enum RateLimitResult - { - OK, - LimitReached - } + // allow 10 requests per 5 seconds, announce to chat if the limit is reached + private readonly RateLimiter rateLimiter = new( + maxRequests: 10, + expiryInSeconds: 5, + punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce)); - private struct RateLimitInfo - { - public int Requests; - public const int MaxRequests = 10; - public DateTimeOffset Expiry; - } + private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); - private readonly Dictionary rateLimits = new Dictionary(); + private readonly List afflictionSubscribers = new(); public void ServerRead(IReadMessage inc, Client sender) { @@ -35,6 +30,9 @@ namespace Barotrauma case NetworkHeader.ADD_EVERYTHING_TO_PENDING: ProcessAddEverything(sender); break; + case NetworkHeader.UNSUBSCRIBE_ME: + RemoveClientSubscription(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -58,7 +56,7 @@ namespace Barotrauma private void ProcessNewAddition(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); @@ -67,14 +65,25 @@ namespace Barotrauma private void ProcessAddEverything(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } AddEverythingToPending(); ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } + private void RemoveClientSubscription(Client client) + { + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Subscriber == client || sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + } + } + } + private void ProcessNewRemoval(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetRemovedAffliction removed = INetSerializableStruct.Read(inc); RemovePendingAffliction(removed.CrewMember, removed.Affliction); @@ -83,14 +92,14 @@ namespace Barotrauma private void ProcessRequestedPending(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } HealRequestResult result = HealAllPending(client: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); @@ -98,7 +107,7 @@ namespace Barotrauma private void ProcessClearing(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } if (!PendingHeals.Any()) { return; } @@ -108,7 +117,7 @@ namespace Barotrauma private void ProcessRequestedAfflictions(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember crewMember = INetSerializableStruct.Read(inc); @@ -129,35 +138,17 @@ namespace Barotrauma Afflictions = pendingAfflictions }; + if (foundInfo is not null) + { + RemoveClientSubscription(client); + + // the client subscribes to the afflictions of the crew member for the next minute + afflictionSubscribers.Add(new AfflictionSubscriber(client, foundInfo, DateTimeOffset.Now.AddMinutes(1))); + } + ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); } - private RateLimitResult CheckRateLimit(Client client) - { - if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo)) - { - if (rateLimitInfo.Expiry < DateTimeOffset.Now) - { - rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5); - rateLimitInfo.Requests = 1; - } - else - { - if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; } - - rateLimitInfo.Requests++; - } - - rateLimits[client] = rateLimitInfo; - } - else - { - rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) }); - } - - return RateLimitResult.OK; - } - private IWriteMessage StartSending() { IWriteMessage msg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index de8517914..188721aa1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -8,10 +8,12 @@ namespace Barotrauma.Items.Components private readonly struct EventData : IEventData { public readonly bool Launch; + public readonly byte SpreadCounter; - public EventData(bool launch) + public EventData(bool launch, byte spreadCounter = 0) { Launch = launch; + SpreadCounter = spreadCounter; } } @@ -32,6 +34,7 @@ namespace Barotrauma.Items.Components msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); + msg.WriteByte(eventData.SpreadCounter); } bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 7ba476df9..9403732da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components //already connected, no need to do anything if (Connections[i].Wires.Contains(newWire)) { continue; } - newWire.Connect(Connections[i], true, true); + newWire.TryConnect(Connections[i], true, true); Connections[i].TryAddLink(newWire); var otherConnection = newWire.OtherConnection(Connections[i]); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 8beb42942..fd03a0bfd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components { string newOutputValue = msg.ReadString(); - if (item.CanClientAccess(c)) + if (item.CanClientAccess(c) && !Readonly) { if (newOutputValue.Length > MaxMessageLength) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 65b176cff..50e6146bb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -135,6 +136,8 @@ namespace Barotrauma } } + EnsureItemsInBothHands(c.Character); + CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { @@ -174,6 +177,29 @@ namespace Barotrauma } } + private void EnsureItemsInBothHands(Character character) + { + if (this is not CharacterInventory charInv) { return; } + + int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand), + rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand); + + TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot); + TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot); + + void TryPutInOppositeHandSlot(int originalSlot, int otherHandSlot) + { + const InvSlotType bothHandSlot = InvSlotType.LeftHand | InvSlotType.RightHand; + + foreach (Item it in slots[originalSlot].Items) + { + if (it.AllowedSlots.None(static s => s.HasFlag(bothHandSlot)) || slots[otherHandSlot].Contains(it)) { continue; } + + TryPutItem(it, otherHandSlot, true, true, character, false); + } + } + } + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 3508a03ae..a48f13895 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -65,7 +65,8 @@ namespace Barotrauma msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case ItemStatusEventData _: + case ItemStatusEventData statusEvent: + msg.WriteBoolean(statusEvent.LoadingRound); msg.WriteSingle(condition); break; case AssignCampaignInteractionEventData _: @@ -253,6 +254,7 @@ namespace Barotrauma msg.WriteBoolean(hasIdCard); if (hasIdCard) { + msg.WriteInt32(idCardComponent.SubmarineSpecificID); msg.WriteString(idCardComponent.OwnerName); msg.WriteString(idCardComponent.OwnerTags); msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index c2a1f8cc6..8742d17eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -140,8 +140,7 @@ namespace Barotrauma.Networking return Option.Some(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); } - bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement) - .OfType>().Select(o => o.Value)); + bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement).NotNone()); } private void RemoveExpired() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index c285765e3..e9216ab7b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -102,9 +102,9 @@ namespace Barotrauma.Networking similarity *= 0.25f; } - bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection; + bool isSpamExempt = RateLimiter.IsExempt(c); - if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner) + if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) { GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); @@ -125,7 +125,7 @@ namespace Barotrauma.Networking c.ChatSpamSpeed += similarity + 0.5f; - if (c.ChatSpamTimer > 0.0f && !isOwner) + if (c.ChatSpamTimer > 0.0f && !isSpamExempt) { ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 8c9a90c95..cb1f3d105 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Networking } else { - var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); + var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None"); if (defaultPerms != null) { newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); @@ -332,9 +332,8 @@ namespace Barotrauma.Networking public void Update(float deltaTime) { -#if CLIENT - if (ShowNetStats) { netStats.Update(deltaTime); } -#endif + dosProtection.Update(deltaTime); + if (!started) { return; } if (ChildServerRelay.HasShutDown) @@ -387,10 +386,10 @@ namespace Barotrauma.Networking Voting.Update(deltaTime); bool isCrewDead = - connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); + connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated)); bool subAtLevelEnd = false; - if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode)) + if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode) { if (Level.Loaded?.EndOutpost != null) { @@ -692,10 +691,14 @@ namespace Barotrauma.Networking } } + private readonly DoSProtection dosProtection = new(); + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); + using var _ = dosProtection.Start(connectedClient); + ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { @@ -784,7 +787,7 @@ namespace Barotrauma.Networking if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); - return; + break; } if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { @@ -1085,7 +1088,7 @@ namespace Barotrauma.Networking ChatMessage.ServerRead(inc, c); break; case ClientNetSegment.Vote: - Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c, dosProtection); break; default: return SegmentTableReader.BreakSegmentReading.Yes; @@ -1116,6 +1119,7 @@ namespace Barotrauma.Networking { //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } + MissionAction.NotifyMissionsUnlockedThisRound(c); c.InGame = true; } } @@ -1245,7 +1249,7 @@ namespace Barotrauma.Networking entityEventManager.Read(inc, c); break; case ClientNetSegment.Vote: - Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c, dosProtection); break; case ClientNetSegment.SpectatingPos: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); @@ -1405,19 +1409,23 @@ namespace Barotrauma.Networking bool quitCampaign = inc.ReadBoolean(); if (GameStarted) { - Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) + using (dosProtection.Pause(sender)) { - mpCampaign.SavePlayers(); - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - mpCampaign.UpdateStoreStock(); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); + if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) + { + mpCampaign.SavePlayers(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + mpCampaign.UpdateStoreStock(); + GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + save = false; + } + EndGame(wasSaved: save); } - else - { - save = false; - } - EndGame(wasSaved: save); } else if (mpCampaign != null) { @@ -1441,45 +1449,54 @@ namespace Barotrauma.Networking } else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + using (dosProtection.Pause(sender)) + { + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + } } } else if (!GameStarted && !initiatedStartGame) { - Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - TryStartGame(); + using (dosProtection.Pause(sender)) + { + Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); + TryStartGame(); + } } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { - var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); - //don't force location if we've teleported - bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; - switch (availableTransition) + using (dosProtection.Pause(sender)) { - case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: - if (forceLocation) - { - mpCampaign.Map.SelectLocation( - mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); - } - mpCampaign.LoadNewLevel(); - break; - case CampaignMode.TransitionType.ProgressToNextEmptyLocation: - if (forceLocation) - { - mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); - } - mpCampaign.LoadNewLevel(); - break; - case CampaignMode.TransitionType.None: -#if DEBUG || UNSTABLE - DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); -#endif - return; - default: - Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - mpCampaign.LoadNewLevel(); - break; + var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); + //don't force location if we've teleported + bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; + switch (availableTransition) + { + case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: + if (forceLocation) + { + mpCampaign.Map.SelectLocation( + mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); + } + mpCampaign.LoadNewLevel(); + break; + case CampaignMode.TransitionType.ProgressToNextEmptyLocation: + if (forceLocation) + { + mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); + } + mpCampaign.LoadNewLevel(); + break; + case CampaignMode.TransitionType.None: + #if DEBUG || UNSTABLE + DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); + #endif + break; + default: + Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + mpCampaign.LoadNewLevel(); + break; + } } } } @@ -1519,11 +1536,7 @@ namespace Barotrauma.Networking mpCampaign?.ServerRead(inc, sender); break; case ClientPermissions.ConsoleCommands: - { - string consoleCommand = inc.ReadString(); - Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); - DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand); - } + DebugConsole.ServerRead(inc, sender); break; case ClientPermissions.ManagePermissions: byte targetClientID = inc.ReadByte(); @@ -2366,10 +2379,7 @@ namespace Barotrauma.Networking List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); - if (Level.Loaded?.StartOutpost != null && - Level.Loaded.Type == LevelData.LevelType.Outpost && - (Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && - Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) + if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && @@ -2444,7 +2454,6 @@ namespace Barotrauma.Networking spawnedCharacter.Info.InventoryData = new XElement("inventory"); spawnedCharacter.Info.StartItemsGiven = true; spawnedCharacter.SaveInventory(); - // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway spawnedCharacter.LoadTalents(); } } @@ -2979,7 +2988,7 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (client.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (client.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) @@ -3313,12 +3322,13 @@ namespace Barotrauma.Networking if (checkActiveVote && Voting.ActiveVote != null) { +#warning TODO: this is mostly the same as Voting.Update, deduplicate (if/when refactoring the Voting class?) var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); - if (inGameClients.Count() == 1) + if (inGameClients.Count() == 1 && inGameClients.First() == Voting.ActiveVote.VoteStarter) { Voting.ActiveVote.Finish(Voting, passed: true); } - else + else if (inGameClients.Any()) { var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter); int yes = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 2); @@ -3388,12 +3398,11 @@ namespace Barotrauma.Networking public void SwitchSubmarine() { - if (!(Voting.ActiveVote is Voting.SubmarineVote subVote)) { return; } + if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; } SubmarineInfo targetSubmarine = subVote.Sub; VoteType voteType = Voting.ActiveVote.VoteType; Client starter = Voting.ActiveVote.VoteStarter; - int deliveryFee = 0; switch (voteType) { @@ -3403,7 +3412,6 @@ namespace Barotrauma.Networking GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: - deliveryFee = subVote.DeliveryFee; break; default: return; @@ -3411,7 +3419,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter); } Voting.StopSubmarineVote(true); @@ -3592,15 +3600,28 @@ namespace Barotrauma.Networking } } + private readonly RateLimiter charInfoRateLimiter = new( + maxRequests: 5, + expiryInSeconds: 10, + punishmentRules: new[] + { + (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce), + (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick) + }); + private void UpdateCharacterInfo(IReadMessage message, Client sender) { - sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); - if (sender.SpectateOnly) - { - return; - } + bool spectateOnly = message.ReadBoolean(); + message.ReadPadBits(); - string newName = message.ReadString(); + sender.SpectateOnly = spectateOnly && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); + if (sender.SpectateOnly) { return; } + + var netInfo = INetSerializableStruct.Read(message); + + if (charInfoRateLimiter.IsLimitReached(sender)) { return; } + + string newName = netInfo.NewName; if (string.IsNullOrEmpty(newName)) { newName = sender.Name; @@ -3618,42 +3639,31 @@ namespace Barotrauma.Networking } } - int tagCount = message.ReadByte(); - HashSet tagSet = new HashSet(); - for (int i = 0; i < tagCount; i++) - { - tagSet.Add(message.ReadIdentifier()); - } - int hairIndex = message.ReadByte(); - int beardIndex = message.ReadByte(); - int moustacheIndex = message.ReadByte(); - int faceAttachmentIndex = message.ReadByte(); - Color skinColor = message.ReadColorR8G8B8(); - Color hairColor = message.ReadColorR8G8B8(); - Color facialHairColor = message.ReadColorR8G8B8(); - - List jobPreferences = new List(); - int count = message.ReadByte(); - for (int i = 0; i < Math.Min(count, 3); i++) - { - string jobIdentifier = message.ReadString(); - int variant = message.ReadByte(); - if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab)) - { - if (jobPrefab.HiddenJob) { continue; } - jobPreferences.Add(new JobVariant(jobPrefab, variant)); - } - } - sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName); - sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - sender.CharacterInfo.Head.SkinColor = skinColor; - sender.CharacterInfo.Head.HairColor = hairColor; - sender.CharacterInfo.Head.FacialHairColor = facialHairColor; - if (jobPreferences.Count > 0) + sender.CharacterInfo.RecreateHead( + tags: netInfo.Tags.ToImmutableHashSet(), + hairIndex: netInfo.HairIndex, + beardIndex: netInfo.BeardIndex, + moustacheIndex: netInfo.MoustacheIndex, + faceAttachmentIndex: netInfo.FaceAttachmentIndex); + + sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor; + sender.CharacterInfo.Head.HairColor = netInfo.HairColor; + sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor; + + if (netInfo.JobVariants.Length > 0) { - sender.JobPreferences = jobPreferences; + List variants = new List(); + foreach (NetJobVariant jv in netInfo.JobVariants) + { + if (jv.ToJobVariant() is { } variant) + { + variants.Add(variant); + } + } + + sender.JobPreferences = variants; } } @@ -3973,8 +3983,7 @@ namespace Barotrauma.Networking } public void Quit() - { - + { if (started) { started = false; @@ -3986,7 +3995,7 @@ namespace Barotrauma.Networking ServerSettings.SaveSettings(); - ModSender.Dispose(); + ModSender?.Dispose(); if (ServerSettings.SaveServerLogs) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index e732ce117..737e9555d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -158,7 +158,7 @@ namespace Barotrauma else if (client.Karma < 40.0f) herpesStrength = 30.0f; - var existingAffliction = client.Character.CharacterHealth.GetAffliction("spaceherpes"); + var existingAffliction = client.Character.CharacterHealth.GetAffliction(AfflictionPrefab.SpaceHerpesType); if (existingAffliction == null && herpesStrength > 0.0f) { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); @@ -170,7 +170,7 @@ namespace Barotrauma existingAffliction.Strength = herpesStrength; if (herpesStrength <= 0.0f) { - client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f); + client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.InvertControlsType, 100.0f); } } @@ -358,8 +358,8 @@ namespace Barotrauma } } - bool targetIsHusk = target.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; - bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; + bool targetIsHusk = target.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; + bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; //huskified characters count as enemies to healthy characters and vice versa if (targetIsHusk != attackerIsHusk) { isEnemy = true; } @@ -614,7 +614,7 @@ namespace Barotrauma if (amount < 0.0f) { - float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); var clientMemory = GetClientMemory(client); clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index b231a8672..b60d34661 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId); + PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId); DebugConsole.Log($"{steamId} validation: {status}, {(pendingClient != null)}"); if (pendingClient is null) @@ -306,7 +306,7 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { return; } if (connectedClients.Find(c - => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) + => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId) is LidgrenConnection connection) { Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); @@ -380,7 +380,7 @@ namespace Barotrauma.Networking lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); - if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (conn.AccountInfo.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } } lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 4fd2c36a1..b8e4393bf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Networking protected List connectedClients = null!; protected List pendingClients = null!; protected ServerSettings serverSettings = null!; - protected Option ownerKey = null!; + protected Option ownerKey = Option.None; protected NetworkConnection? OwnerConnection; protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) @@ -290,7 +290,7 @@ namespace Barotrauma.Networking pendingClients.Remove(pendingClient); - if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId is Some { Value: SteamId steamId }) + if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId.TryUnwrap(out var steamId)) { Steam.SteamManager.StopAuthSession(steamId); pendingClient.Connection.SetAccountInfo(AccountInfo.None); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 17d19a55f..bc268f9dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -218,7 +218,10 @@ namespace Barotrauma.Networking foreach (Door door in shuttleDoors) { - if (door.IsOpen) door.TrySetState(false, false, true); + if (door.IsOpen) + { + door.TrySetState(open: false, isNetworkMessage: false, sendNetworkMessage: true); + } } var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 7e348523a..55a116898 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag.Keys .Where(k => IsFlagRequired(c, k)) .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); - + partial void InitProjSpecific() { LoadSettings(); @@ -176,7 +176,11 @@ namespace Barotrauma.Networking netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { - GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); + GameServer.Log( + NetworkMember.ClientLogName(c) + + $" changed {netProperties[key].Name}" + + $" to {netProperties[key].Value}", + ServerLog.MessageType.ServerMessage); } propertiesChanged = true; } @@ -330,6 +334,10 @@ namespace Barotrauma.Networking { LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } + if (string.IsNullOrEmpty(doc.Root.GetAttributeString("language", ""))) + { + Language = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); @@ -512,7 +520,7 @@ namespace Barotrauma.Networking else { string presetName = clientElement.GetAttributeString("preset", ""); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName == presetName); if (preset == null) { DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found."); @@ -577,8 +585,7 @@ namespace Barotrauma.Networking foreach (SavedClientPermission clientPermission in ClientPermissions) { var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands)); - #warning TODO: this is broken because of localization - if (matchingPreset != null && matchingPreset.Name == "None") + if (matchingPreset != null && matchingPreset.Identifier == "None") { continue; } @@ -592,7 +599,7 @@ namespace Barotrauma.Networking clientElement.Add(matchingPreset == null ? new XAttribute("permissions", clientPermission.Permissions.ToString()) - : new XAttribute("preset", matchingPreset.Name)); + : new XAttribute("preset", matchingPreset.DisplayName)); if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 3eaee94aa..4af593e86 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -28,13 +28,11 @@ namespace Barotrauma public SubmarineInfo Sub; public bool TransferItems; - public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, VoteType voteType) { Sub = subInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; VoteStarter = starter; @@ -81,10 +79,10 @@ namespace Barotrauma if (passed) { Wallet fromWallet = From == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : From.Character?.Wallet; - if (fromWallet.TryDeduct(TransferAmount)) + if (fromWallet != null && fromWallet.TryDeduct(TransferAmount)) { Wallet toWallet = To == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : To.Character?.Wallet; - toWallet.Give(TransferAmount); + toWallet?.Give(TransferAmount); } } else @@ -109,7 +107,6 @@ namespace Barotrauma sender, subInfo, transferItems, - voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); @@ -206,12 +203,16 @@ namespace Barotrauma // Do not take unanswered into account for total int yes = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 2); int no = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 1); - int total = Math.Max(yes + no, 1); - - bool passed = - yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || - inGameClients.Count() == 1; + int total = yes + no; + bool passed = false; + //total can be zero if the client who initiated the vote has left + if (total > 0) + { + passed = + yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || + inGameClients.Count() == 1; + } ActiveVote.Finish(this, passed); } } @@ -224,7 +225,7 @@ namespace Barotrauma } } - public void ServerRead(IReadMessage inc, Client sender) + public void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection) { if (GameMain.Server == null || sender == null) { return; } @@ -336,7 +337,10 @@ namespace Barotrauma inc.ReadPadBits(); - GameMain.Server.UpdateVoteStatus(); + using (dosProtection.Pause(sender)) + { + GameMain.Server.UpdateVoteStatus(); + } } public void ServerWrite(IWriteMessage msg) @@ -436,7 +440,6 @@ namespace Barotrauma var subVote = ActiveVote as SubmarineVote; msg.WriteString(subVote.Sub.Name); msg.WriteBoolean(subVote.TransferItems); - msg.WriteInt16((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 993323acb..2a127c1ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -158,7 +158,10 @@ namespace Barotrauma sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + + (!ContentPackageManager.EnabledPackages.All.Any() ? + "None" : + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})")))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index eb1a00ee9..ff3f08bbe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -204,20 +204,24 @@ namespace Barotrauma public void RandomizeSettings() { - if (GameMain.Server.ServerSettings.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8); + if (GameMain.Server.ServerSettings.RandomizeSeed) { LevelSeed = ToolBox.RandomSeed(8); } - if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + //don't touch any of these settings if a campaign is running! + if (GameMain.GameSession?.Campaign == null) { - var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); - SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; - } - if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) - { - var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); - SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; - } + if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + { + var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); + SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; + } + if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) + { + var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); + SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; + } - GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 18e6c142d..56b57530c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -5,12 +5,13 @@ namespace Barotrauma.Steam { partial class SteamManager { - private static void InitializeProjectSpecific() { IsInitialized = true; } + private static void InitializeProjectSpecific() { } + + private static bool IsInitializedProjectSpecific + => Steamworks.SteamServer.IsValid; public static bool CreateServer(Networking.GameServer server, bool isPublic) { - IsInitialized = true; - Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") { GamePort = (ushort)server.Port, @@ -56,10 +57,15 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); - Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp - => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); + int index = 0; + foreach (var contentPackage in contentPackages) + { + string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty; + Steamworks.SteamServer.SetKey( + $"contentpackage{index}", + contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr); + index++; + } Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); @@ -71,6 +77,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); + Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs new file mode 100644 index 000000000..faf50c98a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs @@ -0,0 +1,232 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal sealed class DoSProtection + { + /// + /// A struct that executes an action when it's created and another one when it's disposed. + /// + public readonly ref struct DoSAction + { + private readonly Client sender; + private readonly Action end; + + public DoSAction(Client sender, Action start, Action end) + { + this.sender = sender; + this.end = end; + start(sender); + } + + public void Dispose() + { + end(sender); + } + } + + private sealed class OffenseData + { + /// + /// Timer that keeps track of how long it takes to process a packet. + /// + public readonly Stopwatch Stopwatch = new(); + + /// + /// Amount of strikes the client has received for causing the server to slow down. + /// + public int Strikes; + + /// + /// How many packets have been sent in the last minute. + /// + public int PacketCount; + + /// + /// Resets the strikes and packet count. + /// + public void ResetStrikes() + { + Strikes = 0; + PacketCount = 0; + } + + /// + /// Resets the timer. + /// + public void ResetTimer() => Stopwatch.Reset(); + } + + private readonly Dictionary clients = new(); + + private float stopwatchResetTimer, + strikesResetTimer; + + private const int StopwatchResetInterval = 1, + StrikesResetInterval = 60, + StrikeThreshold = 6; + + /// + /// Called when the server receives a packet to start logging how much time it takes to process. + /// + /// The client to start a timer for. + /// Nothing useful. Required for the "using" keyword. + /// + /// Calling stop is not required, the timer will be stopped automatically when the function it was started in returns. + /// + /// + /// + /// public void ServerRead(IReadMessage msg, Client c) + /// { + /// // start the timer + /// using var _ = dosProtection.Start(connectedClient); + /// + /// if (condition) + /// { + /// // the timer will be stopped here. + /// return; + /// } + /// + /// ProcessMessage(msg); + /// // the timer will be stopped here. + /// } + /// + /// + public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor); + + /// + /// Temporary pauses the timer for the client. + /// Used when we know a packet is going to slow down the server but we don't want to count it as a strike. + /// For example when a client is starting a round. + /// + /// The client to pause the timer for. + /// Nothing useful. Required for the "using" keyword. + /// + /// Calling resume is not required, the timer will be resumed automatically when the using block ends. + /// + /// + /// + /// using (dos.Pause(client)) + /// { + /// // do something that will slow down the server + /// } + /// // the timer will be resumed here + /// + /// + public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor); + + private void StartFor(Client client) + { + if (!clients.ContainsKey(client)) + { + clients.Add(client, new OffenseData()); + } + + clients[client].Stopwatch.Start(); + } + + private void EndFor(Client client) + { + if (GetData(client) is not { } data) { return; } + + data.PacketCount++; + data.Stopwatch.Stop(); + UpdateOffense(client, data); + } + + // stops the clock but doesn't update offenses + private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop(); + + private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start(); + + private void UpdateOffense(Client client, OffenseData data) + { + if (GameMain.Server?.ServerSettings is not { } settings) { return; } + + // client is sending too many packets, kick them + if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin) + { + AttemptKickClient(client, TextManager.Get("PacketLimitKicked")); + clients.Remove(client); + return; + } + + // if the stopwatch has been running for an entire second without the Update() method resetting it (which it does every second) then something is wrong + if (data.Stopwatch.ElapsedMilliseconds < 100) { return; } + + data.Strikes++; + data.ResetTimer(); + + GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection); + + // too many strikes, get them out of here + if (data.Strikes < StrikeThreshold) { return; } + + if (settings.EnableDoSProtection) + { + AttemptKickClient(client, TextManager.Get("DoSProtectionKicked")); + } + + clients.Remove(client); + + static void AttemptKickClient(Client client, LocalizedString reason) + { + // ReSharper disable once ConvertToConstant.Local + bool doesRateLimitAffectClient = +#if DEBUG + true; // for testing +#else + !RateLimiter.IsExempt(client); +#endif + + if (!doesRateLimitAffectClient) + { + return; + } + + GameMain.Server?.KickClient(client, reason.Value); + } + } + + public void Update(float deltaTime) + { + stopwatchResetTimer += deltaTime; + strikesResetTimer += deltaTime; + + // reset the stopwatch every second + if (stopwatchResetTimer > StopwatchResetInterval) + { + stopwatchResetTimer = 0; + foreach (OffenseData data in clients.Values) + { + data.ResetTimer(); + } + } + + // reset the strikes every minute + if (strikesResetTimer > StrikesResetInterval) + { + strikesResetTimer = 0; + foreach (var (client, data) in clients) + { + if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings) + { + if (data.PacketCount > settings.MaxPacketAmount * 0.9f) + { + GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection); + } + } + + data.ResetStrikes(); + } + } + } + + private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs new file mode 100644 index 000000000..4c6f141c9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Networking; + +namespace Barotrauma +{ + public enum RateLimitAction + { + Invalid, + OnLimitReached, + OnLimitDoubled, + } + + public enum RateLimitPunishment + { + None, // just ignore + Announce, // announce to the server + Kick, // kick the player + Ban // ban the player + } + + internal sealed class RateLimiter + { + private sealed record RateLimit(DateTimeOffset Expiry) + { + public int RequestAmount; + } + + private readonly Dictionary rateLimits = new(); + private readonly HashSet expiredRateLimits = new(); + private readonly Dictionary recentlyAnnouncedOffenders = new(); + + private readonly int maxRequests, expiryInSeconds; + + private readonly ImmutableDictionary punishments; + + public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules) + { + this.maxRequests = maxRequests; + this.expiryInSeconds = expiryInSeconds; + + punishments = punishmentRules.ToImmutableDictionary( + static pair => pair.Action, + static pair => pair.Punishment); + } + + public bool IsLimitReached(Client client) + { +#if !DEBUG + if (IsExempt(client)) { return false; } +#endif + expiredRateLimits.Clear(); + + foreach (var (c, limit) in rateLimits) + { + if (limit.Expiry < DateTimeOffset.Now) + { + expiredRateLimits.Add(c); + } + } + + foreach (Client c in expiredRateLimits) + { + rateLimits.Remove(c); + } + + if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit)) + { + rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds)); + rateLimits.Add(client, rateLimit); + } + + rateLimit.RequestAmount++; + + if (rateLimit.RequestAmount > maxRequests) + { + ProcessPunishment(client, rateLimit.RequestAmount); + return true; + } + + return false; + } + + private void ProcessPunishment(Client client, int requests) + { + bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true }; + + foreach (var (action, punishment) in punishments) + { + switch (action) + { + case RateLimitAction.Invalid: + continue; + case RateLimitAction.OnLimitReached when requests >= maxRequests: + case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2: + switch (punishment) + { + case RateLimitPunishment.None: + continue; + case RateLimitPunishment.Announce: + AnnounceOffender(client); + break; + case RateLimitPunishment.Ban when isDosProtectionEnabled: + GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value); + break; + case RateLimitPunishment.Kick when isDosProtectionEnabled: + GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value); + break; + } + break; + } + } + } + + private void AnnounceOffender(Client client) + { + if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry)) + { + if (expiry > DateTimeOffset.Now) { return; } + + recentlyAnnouncedOffenders.Remove(client); + } + + GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection); + recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds)); + } + + public static bool IsExempt(Client client) => + (GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection) + || client.HasPermission(ClientPermissions.SpamImmunity); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 8e904d0d8..2370283c3 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaShared/Data/languageoptions.xml b/Barotrauma/BarotraumaShared/Data/languageoptions.xml new file mode 100644 index 000000000..c104ddc2e --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/languageoptions.xml @@ -0,0 +1,22 @@ + + diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index 961def545..e42ed985a 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -8,7 +8,7 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents,SpamImmunity"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index f0eae6be8..131be5ec0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -168,6 +168,7 @@ namespace Barotrauma public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; public bool IsSteeringThroughGap { get; protected set; } + public bool IsTryingToSteerThroughGap { get; protected set; } public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { @@ -444,7 +445,7 @@ namespace Barotrauma if (EscapeTarget != null) { var door = EscapeTarget.ConnectedDoor; - bool isClosedDoor = door != null && !door.IsOpen; + bool isClosedDoor = door != null && door.IsClosed; Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; float sqrDist = diff.LengthSquared(); bool isClose = sqrDist < MathUtils.Pow2(100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index bbe7863ec..24eebf8fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -245,11 +245,6 @@ namespace Barotrauma { throw new Exception($"Tried to create an enemy ai controller for human!"); } - if (Character.Params.Group == "human") - { - // Pet - Character.TeamID = CharacterTeamType.FriendlyNPC; - } var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement; targetMemories = new Dictionary(); steeringManager = outsideSteering; @@ -309,17 +304,20 @@ namespace Barotrauma break; } } - + //pets are friendly! + if (PetBehavior != null || Character.Group == "human") + { + Character.TeamID = CharacterTeamType.FriendlyNPC; + } ReevaluateAttacks(); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; - requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); myBodies.Add(Character.AnimController.Collider.FarseerBody); + CreatureMetrics.UnlockInEditor(Character.SpeciesName); } private CharacterParams.AIParams _aiParams; @@ -452,6 +450,7 @@ namespace Barotrauma base.Update(deltaTime); UpdateTriggers(deltaTime); Character.ClearInputs(); + IsTryingToSteerThroughGap = false; Reverse = false; bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); @@ -558,8 +557,9 @@ namespace Barotrauma } } - if (AIParams.CanOpenDoors) + if (Character.Params.UsePathFinding && Character.Params.AI.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); if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) @@ -584,6 +584,7 @@ namespace Barotrauma } else { + // Normally the monsters only use pathing inside submarines, not outside. if (Character.Submarine != null && Character.Params.UsePathFinding) { if (steeringManager != insideSteering) @@ -848,7 +849,7 @@ namespace Barotrauma IsSteeringThroughGap = false; if (SwarmBehavior != null) { - SwarmBehavior.IsActive = State == AIState.Idle && Character.CurrentHull == null; + SwarmBehavior.IsActive = SwarmBehavior.ForceActive || State == AIState.Idle && Character.CurrentHull == null; SwarmBehavior.Refresh(); SwarmBehavior.UpdateSteering(deltaTime); } @@ -876,7 +877,7 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) { - if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) + if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f) { // Steer straight up if very deep SteeringManager.SteeringManual(deltaTime, Vector2.UnitY); @@ -1144,7 +1145,6 @@ namespace Barotrauma return; } } - attackLimbSelectionTimer -= deltaTime; if (AttackLimb == null || attackLimbSelectionTimer <= 0) { @@ -1154,7 +1154,8 @@ namespace Barotrauma AttackLimb = GetAttackLimb(attackWorldPos); } } - + Character targetCharacter = SelectedAiTarget.Entity as Character; + IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; bool canAttack = true; bool pursue = false; if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) @@ -1379,7 +1380,6 @@ namespace Barotrauma float distance = 0; Limb attackTargetLimb = null; - Character targetCharacter = SelectedAiTarget.Entity as Character; if (canAttack) { if (!Character.AnimController.SimplePhysicsEnabled) @@ -1400,29 +1400,29 @@ namespace Barotrauma attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; + Vector2 toTargetOffset = toTarget; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) { if (wallTarget.Structure.Submarine != null) { Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity); - toTarget += margin; + toTargetOffset += margin; } } else if (targetCharacter != null) { Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); - toTarget += margin; + toTargetOffset += margin; } else if (SelectedAiTarget.Entity is MapEntity e) { if (e.Submarine != null) { Vector2 margin = CalculateMargin(e.Submarine.Velocity); - toTarget += margin; + toTargetOffset += margin; } } @@ -1430,7 +1430,7 @@ namespace Barotrauma { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } + if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } float distanceOffset = diff * AttackLimb.attack.Duration; @@ -1439,7 +1439,7 @@ namespace Barotrauma } // Check that we can reach the target - distance = toTarget.Length(); + distance = toTargetOffset.Length(); canAttack = distance < AttackLimb.attack.Range; if (canAttack) { @@ -1523,20 +1523,18 @@ namespace Barotrauma } } Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; + bool updateSteering = true; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso); } - if (steeringLimb == null) { State = AIState.Idle; return; } - var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackLimb != null && AttackLimb.attack.Retreat) { UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1603,7 +1601,7 @@ namespace Barotrauma } } } - else + else if (!IsTryingToSteerThroughGap) { if (AttackLimb.attack.Ranged) { @@ -1624,6 +1622,10 @@ namespace Barotrauma SteeringManager.Reset(); } } + else + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); + } } else { @@ -1662,40 +1664,60 @@ namespace Barotrauma if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } if (selectedTargetingParams == null) { break; } var targetSub = SelectedAiTarget.Entity?.Submarine; - if (targetSub == null) { break; } - float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; - float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); + ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity; + float targetSize = 0; + if (!selectedTargetingParams.IgnoreTargetSize) + { + targetSize = + targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 : + targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100; + } + float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition); + bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0; switch (CirclePhase) { case CirclePhase.Start: - currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity)); inverseDir = false; circleDir = GetDirFromHeadingInRadius(); circleRotation = 0; strikeTimer = 0; blockCheckTimer = 0; breakCircling = false; - float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; - float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset; // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. - circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); - canAttack = false; + float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression); + if (isProgressive) + { + float intensity = ClampIntensity(currentAttackIntensity); + float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity); + circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity)); + } + else + { + circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed; + circleFallbackDistance = maxFallBackDistance; + circleOffset = Rand.Vector(maxRandomOffset); + } + circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor); aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); - if (targetSub.Borders.Width < 1000) + DisableAttacksIfLimbNotRanged(); + if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false }) { breakCircling = true; CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance)) { CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.FallBack; } @@ -1705,52 +1727,76 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + Vector2 targetVelocity = GetTargetVelocity(); + float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f: + targetSize + selectedTargetingParams.CircleStartDistance / 2; + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity)) { strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } - else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) + else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) { CirclePhase = CirclePhase.Advance; } - canAttack = false; + DisableAttacksIfLimbNotRanged(); break; case CirclePhase.FallBack: + updateSteering = false; bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); - if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) + if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Advance; break; } - return; + DisableAttacksIfLimbNotRanged(); + break; case CirclePhase.Advance: - Vector2 subSpeed = targetSub.Velocity; - float requiredDistMultiplier = 1; - // If the target sub is moving fast, just steer towards the target until close enough to strike - if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + Vector2 targetVel = GetTargetVelocity(); + // If the target is moving fast, just steer towards the target + if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed())) { CirclePhase = CirclePhase.CloseIn; } + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + { + if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100) + { + circleRotationSpeed *= 1 + deltaTime; + } + else + { + CirclePhase = CirclePhase.CloseIn; + } + } else { - circleRotation += deltaTime * circleRotationSpeed * circleDir; - if (circleRotation < -360) + float rotationStep = circleRotationSpeed * deltaTime * circleDir; + if (isProgressive) { - circleRotation += 360; + circleRotation += rotationStep; } - else if (circleRotation > 360) + else { - circleRotation -= 360; + circleRotation = rotationStep; } Vector2 targetPos = attackSimPos + circleOffset; - if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + float targetDist = targetSize; + if (targetDist <= 0) + { + targetDist = circleFallbackDistance; + } + if (targetSub != null && AttackLimb?.attack is { Ranged: true }) + { + targetDist += circleFallbackDistance / 2; + } + if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist)) { // Too close to the target point // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; strikeTimer = AttackLimb.attack.CoolDown; @@ -1762,7 +1808,6 @@ namespace Barotrauma break; } steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); - requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); if (IsBlocked(deltaTime, steerPos)) { if (!inverseDir) @@ -1774,7 +1819,7 @@ namespace Barotrauma else if (circleRotationSpeed < 1) { // Then try increasing the rotation speed to change the movement curve - circleRotationSpeed *= 1.1f; + circleRotationSpeed *= 1 + deltaTime; } else if (circleOffset.LengthSquared() > 0.1f) { @@ -1784,16 +1829,24 @@ namespace Barotrauma else { // If we still fail, just steer towards the target - breakCircling = true; + breakCircling = AttackLimb?.attack is { Ranged: false }; + if (!breakCircling) + { + CirclePhase = CirclePhase.FallBack; + } } } } - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackLimb?.attack is { Ranged: false }) { - strikeTimer = AttackLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; + canAttack = false; + float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel); + if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + { + strikeTimer = AttackLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } } - canAttack = false; break; case CirclePhase.Strike: strikeTimer -= deltaTime; @@ -1815,18 +1868,19 @@ namespace Barotrauma return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; } - float GetStrikeDistanceMultiplier(Vector2 subSpeed) + float GetStrikeDistanceMultiplier(Vector2 targetVelocity) { + if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; } float requiredDistMultiplier = 2; - bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; if (isHeading) { requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; - float subSpeedHorizontal = Math.Abs(subSpeed.X); - if (subSpeedHorizontal > 1) + float targetVelocityHorizontal = Math.Abs(targetVelocity.X); + if (targetVelocityHorizontal > 1) { // Reduce the required distance if the target is moving. - requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1)); if (requiredDistMultiplier < 2) { requiredDistMultiplier = 2; @@ -1843,19 +1897,59 @@ namespace Barotrauma return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; } - float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + Vector2 GetTargetVelocity() + { + if (targetSub != null) + { + return targetSub.Velocity; + } + else if (targetCharacter != null) + { + return targetCharacter.AnimController.Collider.LinearVelocity; + } + return Vector2.Zero; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f)); } } - if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + if (updateSteering) { - bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; - bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); - if (fallBack) + if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) { - Reverse = true; - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; + bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); + if (fallBack) + { + Reverse = true; + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + else if (advance) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else + { + if (Character.CurrentHull == null && !canAttack) + { + SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + else + { + SteeringManager.Reset(); + FaceTarget(SelectedAiTarget.Entity); + } + } } - else if (advance) + else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1866,41 +1960,18 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else + if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || + distance == 0 || + distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) || + AttackLimb != null && AttackLimb.attack.Ranged)) { - if (Character.CurrentHull == null && !canAttack) - { - SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); - } - else - { - SteeringManager.Reset(); - FaceTarget(SelectedAiTarget.Entity); - } + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } } - else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) - { - if (pathSteering != null) - { - pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); - } - else - { - SteeringManager.SteeringSeek(steerPos, 10); - } - } - - if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) - { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); - } } } Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; - IDamageable damageTarget = targetEntity as IDamageable; - if (AttackLimb?.attack is Attack { Ranged: true} attack) + if (AttackLimb?.attack is Attack { Ranged: true } attack) { AimRangedAttack(attack, targetEntity); } @@ -1915,12 +1986,21 @@ namespace Barotrauma { AttackLimb.attack.ResetAttackTimer(); } + + void DisableAttacksIfLimbNotRanged() + { + if (AttackLimb?.attack is { Ranged: false }) + { + canAttack = false; + } + } } public void AimRangedAttack(Attack attack, Entity targetEntity) { if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } Character.SetInput(InputType.Aim, false, true); + if (attack.AimRotationTorque <= 0) { return; } Limb limb = GetLimbToRotate(attack); if (limb != null) { @@ -2003,9 +2083,18 @@ namespace Barotrauma float prio = 1 + limb.attack.Priority; if (Character.AnimController.SimplePhysicsEnabled) { return prio; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); - // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. - // We also need a max value that is more than the actual range. - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + float distanceFactor = 1; + if (limb.attack.Ranged) + { + float min = 100; + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist)); + } + else + { + // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. + // We also need a max value that is more than the actual range. + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + } return prio * distanceFactor; } } @@ -2164,7 +2253,9 @@ namespace Barotrauma { if (SelectedAiTarget?.Entity == null) { return false; } if (AttackLimb?.attack == null) { return false; } - if (damageTarget == null) { return false; } + ISpatialEntity spatialTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity; + if (spatialTarget == null) { return false; } + ActiveAttack = AttackLimb.attack; if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -2176,13 +2267,14 @@ namespace Barotrauma return true; } } + if (damageTarget == null) { return false; } ActiveAttack = AttackLimb.attack; if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0) { Limb referenceLimb = GetLimbToRotate(ActiveAttack); if (referenceLimb != null) { - Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition; + Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition; float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); @@ -2200,16 +2292,20 @@ namespace Barotrauma { if (item.RequireAimToUse) { - if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) + if (!Aim(deltaTime, spatialTarget, item)) { // Valid target, but can't shoot -> return true so that it will not be ignored. return true; } } - Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, Character); + if (damageTarget != null) + { + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, Character); + } } } + if (damageTarget == null) { return true; } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); if (!ActiveAttack.IsRunning) @@ -2224,20 +2320,11 @@ namespace Barotrauma Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif } - if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (ActiveAttack.CoolDownTimer > 0) { SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f)); - // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - float greed = AIParams.AggressionGreed; - if (damageTarget is not Barotrauma.Character) - { - // Halve the greed for attacking non-characters. - greed /= 2; - } - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) { @@ -2269,10 +2356,19 @@ namespace Barotrauma private float aimTimer; private float visibilityCheckTimer; private bool canSeeTarget; + private float sinTime; private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { if (target == null || weapon == null) { return false; } + if (AttackLimb == null) { return false; } + Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; + float dist = toTarget.Length(); Character.CursorPosition = target.WorldPosition; + if (AttackLimb.attack.SwayAmount > 0) + { + sinTime += deltaTime * AttackLimb.attack.SwayFrequency; + Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount); + } if (Character.Submarine != null) { Character.CursorPosition -= Character.Submarine.Position; @@ -2294,11 +2390,11 @@ namespace Barotrauma aimTimer -= deltaTime; return false; } - Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget); - float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length())); + float minDistance = 300; + float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist)); float margin = MathHelper.PiOver4 * distanceFactor; - if (angle < margin) + if (angle < margin || dist < minDistance) { var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true); @@ -2591,13 +2687,19 @@ namespace Barotrauma { // Ignore all structures, items, and hulls inside these subs. if (aiTarget.Entity.Submarine != null) - { - if (aiTarget.Entity.Submarine.Info.IsWreck || - aiTarget.Entity.Submarine.Info.IsBeacon || + { + if (aiTarget.Entity.Submarine.Info.IsWreck || + aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) { continue; } + //ignore the megaruin in end levels + if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null && + aiTarget.Entity.Submarine.Info.OutpostGenerationParams.ForceToEndLocationIndex > -1) + { + continue; + } } if (aiTarget.Entity is Hull hull) { @@ -2698,7 +2800,7 @@ namespace Barotrauma } else if (CanPassThroughHole(s, i)) { - valueModifier *= isInnerWall ? 1 : 0; + valueModifier *= isInnerWall ? 0.5f : 0; } else if (!canAttackWalls) { @@ -2968,7 +3070,8 @@ namespace Barotrauma // In the attack state allow going into non-allowed zone only when chasing a target. if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; } } - if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) + bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine; + if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) { // If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state. bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget); @@ -3401,10 +3504,10 @@ namespace Barotrauma private readonly float stateResetCooldown = 10; private float stateResetTimer; private bool isStateChanged; - private readonly Dictionary activeTriggers = new Dictionary(); - private readonly HashSet inactiveTriggers = new HashSet(); + private readonly Dictionary activeTriggers = new Dictionary(); + private readonly HashSet inactiveTriggers = new HashSet(); - public void LaunchTrigger(AITrigger trigger) + public void LaunchTrigger(StatusEffect.AITrigger trigger) { if (trigger.IsTriggered) { return; } if (activeTriggers.ContainsKey(trigger)) { return; } @@ -3424,7 +3527,7 @@ namespace Barotrauma { foreach (var triggerObject in activeTriggers) { - AITrigger trigger = triggerObject.Key; + StatusEffect.AITrigger trigger = triggerObject.Key; if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) @@ -3434,7 +3537,7 @@ namespace Barotrauma inactiveTriggers.Add(trigger); } } - foreach (AITrigger trigger in inactiveTriggers) + foreach (StatusEffect.AITrigger trigger in inactiveTriggers) { activeTriggers.Remove(trigger); } @@ -3588,6 +3691,11 @@ namespace Barotrauma observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); } reachTimer = 0; + sinTime = 0; + if (breakCircling && strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + } } protected override void OnStateChanged(AIState from, AIState to) @@ -3609,6 +3717,11 @@ namespace Barotrauma } blockCheckTimer = 0; reachTimer = 0; + sinTime = 0; + if (breakCircling && strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + } } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); @@ -3712,6 +3825,7 @@ namespace Barotrauma { targetDir = Vector2.Zero; if (Level.Loaded == null) { return true; } + if (Level.Loaded.LevelData.Biome.IsEndBiome) { return true; } if (AIParams.AvoidAbyss) { if (pos.Y < Level.Loaded.AbyssStart) @@ -3771,6 +3885,7 @@ namespace Barotrauma public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { + IsTryingToSteerThroughGap = true; wallTarget = null; LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 919db14e8..9330350dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1,10 +1,10 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.Items.Components; namespace Barotrauma { @@ -395,6 +395,8 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); + UpdateDragged(deltaTime); + if (reportProblemsTimer > 0) { reportProblemsTimer -= deltaTime; @@ -430,7 +432,7 @@ namespace Barotrauma } if (reportProblemsTimer <= 0.0f) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { ReportProblems(); } @@ -444,7 +446,7 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; + bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) { if (Character.CurrentHull == null) @@ -546,12 +548,11 @@ namespace Barotrauma bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { - if (!Character.NeedsAir) { return false; } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; Hull targetHull = gotoObjective.GetTargetHull(); - return gotoObjective.Target != null && targetHull == null || + return (gotoObjective.Target != null && targetHull == null && !Character.IsImmuneToPressure) || NeedsDivingGear(targetHull, out _) || - insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))); + (insideSteering && ((PathSteering.CurrentPath.HasOutdoorsNodes && !Character.IsImmuneToPressure) || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)))); } if (isCarrying) @@ -584,7 +585,7 @@ namespace Barotrauma Character.AnimController.InWater || Character.AnimController.HeadInWater || Character.Submarine == null || - (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || + (!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || @@ -621,9 +622,10 @@ namespace Barotrauma } else if (gotoObjective.Mimic) { + bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false); if (!removeSuit) { - removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); + removeDivingSuit = !targetHasDivingGear; if (removeDivingSuit) { removeSuit = true; @@ -631,7 +633,7 @@ namespace Barotrauma } if (!removeMask) { - takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); + takeMaskOff = !targetHasDivingGear; if (takeMaskOff) { removeMask = true; @@ -783,20 +785,23 @@ namespace Barotrauma private void HandleRelocation(Item item) { - if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC) + if (item.SpawnedInCurrentOutpost) { return; } + if (item.Submarine == null) { return; } + // Only affects bots in the player team + if (!Character.IsOnPlayerTeam) { return; } + // Don't relocate if the item is on a sub of the same team + if (item.Submarine.TeamID == Character.TeamID) { return; } + if (itemsToRelocate.Contains(item)) { return; } + itemsToRelocate.Add(item); + if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) { - if (itemsToRelocate.Contains(item)) { return; } - itemsToRelocate.Add(item); - if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) - { - myPort.OnUnDocked += Relocate; - } - var campaign = GameMain.GameSession.Campaign; - if (campaign != null) - { - // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. - campaign.BeforeLevelLoading += Relocate; - } + myPort.OnUnDocked += Relocate; + } + var campaign = GameMain.GameSession.Campaign; + if (campaign != null) + { + // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. + campaign.BeforeLevelLoading += Relocate; } void Relocate() @@ -907,6 +912,35 @@ namespace Barotrauma return false; } + private float draggedTimer; + private float refuseDraggingTimer; + private const float RefuseDraggingAfter = 10.0f; + private const float RefuseDraggingDuration = 30.0f; + + private void UpdateDragged(float deltaTime) + { + if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; } + + //don't allow player characters who aren't in the same team to drag us for more than x seconds + if (Character.SelectedBy == null || + !Character.SelectedBy.IsPlayer || + Character.SelectedBy.TeamID == Character.TeamID) + { + refuseDraggingTimer -= deltaTime; + return; + } + + draggedTimer += deltaTime; + if (draggedTimer > RefuseDraggingAfter || + (draggedTimer > 0.5f && refuseDraggingTimer > 0.0f)) + { + draggedTimer = 0.0f; + refuseDraggingTimer = RefuseDraggingDuration; + Character.SelectedBy.DeselectCharacter(); + Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f); + } + } + protected void ReportProblems() { Order newOrder = null; @@ -989,8 +1023,8 @@ namespace Barotrauma targetHull = hull; } } - } - foreach (Item item in Item.ItemList) + } + foreach (Item item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) @@ -1209,7 +1243,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { @@ -1523,7 +1557,7 @@ namespace Barotrauma { margin *= 2; } - float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin; + float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null; } @@ -1546,23 +1580,19 @@ namespace Barotrauma public bool NeedsDivingGear(Hull hull, out bool needsSuit) { - if (!Character.NeedsAir) - { - needsSuit = false; - return false; - } needsSuit = false; + bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1; if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { - needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - return true; + needsSuit = !Character.IsProtectedFromPressure; + return needsAir || needsSuit; } if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { - return true; + return needsAir; } return false; } @@ -1641,7 +1671,7 @@ namespace Barotrauma { if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || otherCharacter.Info?.Job == null || - !(otherCharacter.AIController is HumanAIController otherHumanAI) || + otherCharacter.AIController is not HumanAIController otherHumanAI || !otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) { continue; @@ -1654,10 +1684,10 @@ namespace Barotrauma float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage); - if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage); } if (accumulatedDamage <= WarningThreshold) { return; } @@ -1745,12 +1775,14 @@ namespace Barotrauma } if (!someoneSpoke) { - if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (!item.StolenDuringRound && + Level.Loaded?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { var reputationLoss = MathHelper.Clamp( (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss); } item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); @@ -1843,7 +1875,7 @@ namespace Barotrauma } break; case "reportbrokendevices": - foreach (var item in Item.ItemList) + foreach (var item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) @@ -1924,11 +1956,12 @@ namespace Barotrauma bool isCurrentHull = character == Character && character.CurrentHull == hull; if (hull == null) { + float hullSafety = character.IsProtectedFromPressure ? 0 : 100; if (isCurrentHull) { - CurrentHullSafety = character.NeedsAir ? 0 : 100; + CurrentHullSafety = hullSafety; } - return CurrentHullSafety; + return hullSafety; } if (isCurrentHull && visibleHulls == null) { @@ -1936,10 +1969,9 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreWater = character.IsProtectedFromPressure(); - bool ignoreOxygen = HasDivingGear(character); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); - float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { CurrentHullSafety = safety; @@ -1949,15 +1981,33 @@ namespace Barotrauma private static float CalculateHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { - if (hull == null) { return character.NeedsAir ? 0 : 100; } - if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; } + bool isProtectedFromPressure = character.IsProtectedFromPressure; + if (hull == null) { return isProtectedFromPressure ? 100 : 0; } + if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; } // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower. // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD. float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage)); - float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100); - if (!character.NeedsAir) + float waterFactor = 1; + if (!ignoreWater) + { + if (visibleHulls != null) + { + // Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results. + float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume); + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + else + { + float relativeWaterVolume = hull.WaterVolume / hull.Volume; + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + } + if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1) { oxygenFactor = 1; + } + if (isProtectedFromPressure) + { waterFactor = 1; } float fireFactor = 1; @@ -2005,6 +2055,10 @@ namespace Barotrauma public float GetHullSafety(Hull hull, Character character, IEnumerable visibleHulls = null) { + if (hull == null) + { + return CalculateHullSafety(hull, character, visibleHulls); + } if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety)) { hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls)); @@ -2019,6 +2073,10 @@ namespace Barotrauma public static float GetHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { + if (hull == null) + { + return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + } HullSafety hullSafety; if (character.AIController is HumanAIController controller) { @@ -2047,21 +2105,53 @@ namespace Barotrauma bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { return false; } - if (!me.IsSameSpeciesOrGroup(other)) { return false; } - if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) + if (other.IsPet) + { + // Hostile NPCs are hostile to all pets, unless they are in the same team. + if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } + } + else + { + if (!me.IsSameSpeciesOrGroup(other)) { return false; } + } + if (GameMain.GameSession?.GameMode is CampaignMode) + { + if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || + (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) + { + Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; + //NPCs that allow some campaign interaction are not turned hostile by low reputation + if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; } + if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI) + { + return !npcAI.IsInHostileFaction(); + } + } + } + return true; + } + + public bool IsInHostileFaction() + { + if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; } + + Identifier npcFaction = Character.Faction; + Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; + + if (npcFaction.IsEmpty) + { + //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost + npcFaction = currentLocationFaction; + } + if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) { var reputation = campaign.Map?.CurrentLocation?.Reputation; if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) { - return false; + return true; } } - if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet) - { - // Hostile NPCs are hostile to all pets, unless they are in the same team. - return false; - } - return true; + return false; } public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5dc043af1..a98233d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -26,7 +26,7 @@ namespace Barotrauma private float findPathTimer; - private const float buttonPressCooldown = 3; + private const float ButtonPressCooldown = 1; private float checkDoorsTimer; private float buttonPressTimer; @@ -96,7 +96,7 @@ namespace Barotrauma base.Update(speed); float step = 1.0f / 60.0f; checkDoorsTimer -= step; - if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen) + if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen) { buttonPressTimer = 0; } @@ -211,7 +211,7 @@ namespace Barotrauma currentTarget = target; Vector2 currentPos = host.SimPosition; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; - pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0; + pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) @@ -310,7 +310,7 @@ namespace Barotrauma // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically - if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) + if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius) { diff.Y = 0.0f; } @@ -342,7 +342,7 @@ namespace Barotrauma CheckDoorsInPath(); doorsChecked = true; } - if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen) + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsFullyOpen) { // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. Reset(); @@ -395,7 +395,7 @@ namespace Barotrauma } //at the same height as the waypoint float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderSize = (collider.height / 2 + collider.radius) * 1.25f; + float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; if (heightDiff < colliderSize) { float heightFromFloor = character.AnimController.GetHeightFromFloor(); @@ -510,7 +510,7 @@ namespace Barotrauma private bool CanAccessDoor(Door door, Func buttonFilter = null) { if (door.IsBroken) { return true; } - if (!door.IsOpen) + if (door.IsClosed) { if (!door.Item.IsInteractable(character)) { return false; } if (!ShouldBreakDoor(door)) @@ -536,7 +536,7 @@ namespace Barotrauma } foreach (var linked in door.Item.linkedTo) { - if (!(linked is Item linkedItem)) { continue; } + if (linked is not Item linkedItem) { continue; } var button = linkedItem.GetComponent(); if (button == null) { continue; } if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) @@ -694,7 +694,7 @@ namespace Barotrauma if (door.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } else { @@ -712,7 +712,7 @@ namespace Barotrauma if (closestButton.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } else { @@ -785,7 +785,7 @@ namespace Barotrauma { if (hull.WaterVolume / hull.Rect.Width > 100.0f) { - if (!HumanAIController.HasDivingSuit(character)) + if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1) { penalty += 500.0f; } @@ -808,7 +808,7 @@ namespace Barotrauma private float? GetSingleNodePenalty(PathNode node) { - if (node.Waypoint.isObstructed) { return null; } + if (!node.Waypoint.IsTraversable) { return null; } if (node.IsBlocked()) { return null; } float penalty = 0.0f; if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 5dc813a8c..2dcdc2e10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -244,7 +244,7 @@ namespace Barotrauma else { float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos); - float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; + float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f; if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs index 3b74d869d..e273488e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs @@ -107,7 +107,6 @@ namespace Barotrauma { return MentalType.Normal; } - // test this later int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f)); psychosisIndex = Math.Clamp(psychosisIndex, 0, 4); MentalType mentalType = psychosisIndex switch diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 4ddfe05c1..2fb0e0027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -83,11 +83,16 @@ namespace Barotrauma { if (GameMain.GameSession.RoundDuration < 120.0f && speaker?.CurrentHull != null && + GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { currentFlags.Add("EnterOutpost".ToIdentifier()); } + if (Level.Loaded.IsEndBiome) + { + currentFlags.Add("EndLevel".ToIdentifier()); + } } if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f) { @@ -117,7 +122,7 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag)) + if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); } @@ -126,6 +131,10 @@ namespace Barotrauma if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) { currentFlags.Add("OutpostNPC".ToIdentifier()); + if (GameMain.GameSession?.Level?.StartLocation?.Faction is Faction faction) + { + currentFlags.Add($"OutpostNPC{faction.Prefab.Identifier}".ToIdentifier()); + } } if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index cb7523470..3a16cf84d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -256,7 +256,9 @@ namespace Barotrauma if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } if (AllowInAnySub) { return true; } if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; } - return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID); + return character.Submarine.TeamID == character.TeamID || + character.Submarine.TeamID == character.OriginalTeamID || + character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 3957e400a..066b2b9a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -648,11 +648,11 @@ namespace Barotrauma { statusEffects = statusEffects.Concat(hitEffects); } - float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0); + float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => { float stunAmount = 0; - var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun"); + var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); if (stunAffliction != null) { stunAmount = stunAffliction.Strength; @@ -1176,30 +1176,31 @@ namespace Barotrauma if (sqrDistance > repairTool.Range * repairTool.Range) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); - if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) { if (myBodies == null) { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); - if (pickedBody != null) + // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) + var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); + foreach (var body in pickedBodies) { Character target = null; - if (pickedBody.UserData is Character c) + if (body.UserData is Character c) { target = c; } - else if (pickedBody.UserData is Limb limb) + else if (body.UserData is Limb limb) { target = limb.character; } - if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target))) + if (target != null && target != Enemy && HumanAIController.IsFriendly(target)) { - UseWeapon(deltaTime); + return; } } + UseWeapon(deltaTime); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 08d7ea70c..4c1d874d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -200,7 +201,8 @@ namespace Barotrauma (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, - SpeakIfFails = !objectiveManager.IsCurrentOrder() + SpeakIfFails = !objectiveManager.IsCurrentOrder(), + endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 5dcbdb17e..aee20f6ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -19,7 +19,6 @@ namespace Barotrauma private AIObjectiveGetItem getExtinguisherObjective; private AIObjectiveGoTo gotoObjective; - private float useExtinquisherTimer; public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -44,7 +43,8 @@ namespace Barotrauma } else { - float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y); yDist = yDist > 100 ? yDist * 3 : 0; float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); @@ -119,24 +119,18 @@ namespace Barotrauma Abandon = true; break; } - float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; - float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); - bool inRange = xDist + yDist < extinguisher.Range; - // Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall. - ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity; - bool move = !inRange || !character.CanSeeTarget(lookTarget); - if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0) + float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X); + float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y); + float dist = xDist + yDist; + bool inRange = dist < extinguisher.Range; + bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull); + bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull)); + bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull)); + if (operateExtinguisher) { - useExtinquisherTimer += deltaTime; - if (useExtinquisherTimer > 2.0f) - { - useExtinquisherTimer = 0.0f; - } - // Aim character.CursorPosition = fs.Position; Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; - float dist = fromCharacterToFireSource.Length(); - character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2); if (extinguisherItem.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); @@ -148,25 +142,29 @@ namespace Barotrauma { character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } + // Prevents running into the flames. + objectiveManager.CurrentObjective.ForceWalk = true; } - if (move) + if (moveCloser) { - //go to the first firesource - if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) - { - DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), - TargetName = fs.Hull.DisplayName - }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref gotoObjective))) + if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f) + { + DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), + TargetName = fs.Hull.DisplayName, + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref gotoObjective))) { gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull); } } - else + else if (!operateExtinguisher || isInDamageRange) { - character.AIController.SteeringManager.Reset(); + // Don't walk into the flames. + RemoveSubObjective(ref gotoObjective); + SteeringManager.Reset(); } + // Only target one fire source at the time. break; } } @@ -177,8 +175,20 @@ namespace Barotrauma base.Reset(); getExtinguisherObjective = null; gotoObjective = null; - useExtinquisherTimer = 0; sinTime = 0; + SteeringManager.Reset(); + } + + protected override void OnCompleted() + { + base.OnCompleted(); + SteeringManager.Reset(); + } + + protected override void OnAbandon() + { + base.OnAbandon(); + SteeringManager.Reset(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index e525b613c..7bbe5e18b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -24,7 +24,7 @@ namespace Barotrauma protected override float TargetEvaluation() { if (Targets.None()) { return 0; } - if (!character.IsOnPlayerTeam) { return 100; } + if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. @@ -66,7 +66,13 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; } + if (!targetCharactersInOtherSubs) + { + if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID) + { + return false; + } + } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index e299a8edb..221067c28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -47,19 +47,12 @@ namespace Barotrauma } if (character.CurrentHull == null) { - if (!character.NeedsAir) - { - Priority = 0; - } - else - { - Priority = ( - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasActiveObjective() || - objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) - && HumanAIController.HasDivingSuit(character) ? 0 : 100; - } + Priority = ( + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasActiveObjective() || + objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + && ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100; } else { @@ -118,6 +111,11 @@ namespace Barotrauma if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { Priority -= priorityDecrease * deltaTime; + if (currenthullSafety >= 100) + { + // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. + Priority = 0; + } } else { @@ -140,8 +138,8 @@ namespace Barotrauma { if (resetPriority) { return; } var currentHull = character.CurrentHull; + bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); - bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); @@ -221,7 +219,11 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { - AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) + AllowGoingOutside = + character.IsProtectedFromPressure || + character.CurrentHull == null || + character.CurrentHull.IsTaggedAirlock() || + character.CurrentHull.LeadsOutside(character) }, onCompleted: () => { @@ -352,8 +354,8 @@ namespace Barotrauma //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive //path calculations, only to discard all of them when going through the hulls in the outpost) - float hullSuitability = EstimateHullSuitability(character, hull); - if (!hulls.Any()) + float hullSuitability = EstimateHullSuitability(character, hull); + if (hulls.None()) { hulls.Add(hull); } @@ -448,9 +450,12 @@ namespace Barotrauma { hullSafety = 100; } + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; // Huge preference for closer targets - float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition); - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9604579dd..dd0b1e20b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -155,17 +155,21 @@ namespace Barotrauma bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak), - onAbandon: () => Abandon = true, - onCompleted: () => + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak) + { + // Use an empty filter to override the default + EndNodeFilter = n => true + }, + onAbandon: () => Abandon = true, + onCompleted: () => + { + if (CheckObjectiveSpecific()) { IsCompleted = true; } + else { - if (CheckObjectiveSpecific()) { IsCompleted = true; } - else - { - // Failed to operate. Probably too far. - Abandon = true; - } - }); + // Failed to operate. Probably too far. + Abandon = true; + } + }); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index b3cdaed85..2f3e75a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -123,6 +123,11 @@ namespace Barotrauma return ignoredTags; } + public static Func CreateEndNodeFilter(ISpatialEntity targetEntity) + { + return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); + } + private bool CheckInventory() { if (IdentifiersOrTags == null) { return false; } @@ -155,11 +160,6 @@ namespace Barotrauma Abandon = true; return; } - if (character.Submarine == null) - { - Abandon = true; - return; - } if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -171,9 +171,14 @@ namespace Barotrauma } if (!isDoneSeeking) { + if (character.Submarine == null) + { + Abandon = true; + return; + } if (!AllowDangerousPressure) { - bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0; + bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0); if (dangerousPressure) { #if DEBUG @@ -192,6 +197,11 @@ namespace Barotrauma return; } } + else if (character.Submarine == null) + { + Abandon = true; + return; + } if (targetItem == null || targetItem.Removed) { #if DEBUG @@ -307,7 +317,8 @@ namespace Barotrauma { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, - SpeakIfFails = false + SpeakIfFails = false, + endNodeFilter = CreateEndNodeFilter(moveToTarget) }; }, onAbandon: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 684713e02..e3a5efe07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -33,6 +33,11 @@ namespace Barotrauma public bool DebugLogWhenFails { get; set; } = true; public bool UsePathingOutside { get; set; } = true; + /// + /// Which event action created this objective (if any) + /// + public EventAction SourceEventAction; + public float ExtraDistanceWhileSwimming; public float ExtraDistanceOutsideSub; private float _closeEnoughMultiplier = 1; @@ -45,6 +50,7 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; + private bool cantFindDivingGear; /// /// Display units @@ -85,7 +91,7 @@ namespace Barotrauma /// public bool UseDistanceRelativeToAimSourcePos { get; set; } = false; - public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; @@ -258,48 +264,73 @@ namespace Barotrauma } if (!Abandon) { - if (getDivingGearIfNeeded && !character.LockHands) + if (getDivingGearIfNeeded) { Character followTarget = Target as Character; - bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); - if (Mimic) + bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure; + bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); + bool tryToGetDivingSuit = needsDivingSuit; + if (Mimic && !character.IsImmuneToPressure) { if (HumanAIController.HasDivingSuit(followTarget)) { - needsDivingGear = true; - needsDivingSuit = true; + tryToGetDivingGear = true; + tryToGetDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget)) + else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) { - needsDivingGear = true; + tryToGetDivingGear = true; } } bool needsEquipment = false; float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); - if (needsDivingSuit) + if (tryToGetDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); } - else if (needsDivingGear) + else if (tryToGetDivingGear) { needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); } - if (needsEquipment) + if (character.LockHands) + { + cantFindDivingGear = true; + } + if (cantFindDivingGear && needsDivingSuit) + { + // Don't try to reach the target without a suit because it's lethal. + Abandon = true; + return; + } + if (needsEquipment && !cantFindDivingGear) { SteeringManager.Reset(); - if (findDivingGear != null && !findDivingGear.CanBeCompleted) - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } - else - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), + onAbandon: () => + { + cantFindDivingGear = true; + if (needsDivingSuit) + { + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; + } + else + { + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => + { + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); + } + }, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); return; } } @@ -593,7 +624,7 @@ namespace Barotrauma } else if (target is Character c) { - return c.CurrentHull; + return c.CurrentHull ?? c.AnimController.CurrentHull; } else if (target is Structure structure) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 72afb8181..3625f7a1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -170,7 +170,8 @@ namespace Barotrauma TargetHull = character.CurrentHull; } - if (behavior == BehaviorType.StayInHull) + bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -190,9 +191,6 @@ namespace Barotrauma } else { - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || - (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (currentTarget != null && !currentTargetIsInvalid) { if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index b8c94aca0..ec2a5ad30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -98,7 +98,7 @@ namespace Barotrauma { foreach (var item in itemContainer.ContainableItems) { - if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished) + if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished) { return CheckStatus.Finished; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index aed77cb77..06305e134 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -23,6 +23,11 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveGetItem getItemObjective; + /// + /// If undefined, a default filter will be used. + /// + public Func EndNodeFilter; + public bool Override { get; set; } = true; public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); @@ -222,7 +227,7 @@ namespace Barotrauma { target.Item.TryInteract(character, forceSelectKey: true); } - if (component.AIOperate(deltaTime, character, this)) + if (component.CrewAIOperate(deltaTime, character, this)) { isDoneOperating = completionCondition == null || completionCondition(); } @@ -232,7 +237,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50) { TargetName = target.Item.Name, - endNodeFilter = node => node.Waypoint.Ladders == null + endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); @@ -290,7 +295,7 @@ namespace Barotrauma } return; } - if (component.AIOperate(deltaTime, character, this)) + if (component.CrewAIOperate(deltaTime, character, this)) { isDoneOperating = completionCondition == null || completionCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index e8b5dea26..9c06fb8b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -17,7 +17,6 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem refuelObjective; - private float previousCondition = -1; private RepairTool repairTool; private const float WaitTimeBeforeRepair = 0.5f; @@ -196,15 +195,7 @@ namespace Barotrauma Abandon = true; } } - if (previousCondition == -1) - { - previousCondition = Item.Condition; - } - else if (Item.Condition < previousCondition) - { - // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. - Abandon = true; - } + CheckPreviousCondition(deltaTime); } if (Abandon) { @@ -229,7 +220,6 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => { - previousCondition = -1; var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { TargetName = Item.Name @@ -251,6 +241,27 @@ namespace Barotrauma } } + private const float conditionCheckDelay = 1; + private float conditionCheckTimer; + private float previousCondition; + private void CheckPreviousCondition(float deltaTime) + { + if (Item == null || Item.Removed) { return; } + conditionCheckTimer -= deltaTime; + if (conditionCheckTimer > 0) { return; } + conditionCheckTimer = conditionCheckDelay; + if (previousCondition > -1 && Item.Condition < previousCondition) + { + // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. + Abandon = true; + } + else + { + // If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here. + previousCondition = Item.Condition; + } + } + private void FindRepairTool() { foreach (Repairable repairable in Item.Repairables) @@ -303,7 +314,6 @@ namespace Barotrauma base.Reset(); goToObjective = null; refuelObjective = null; - previousCondition = -1; repairTool = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index b75a3152e..5a93ceb26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -139,14 +139,14 @@ namespace Barotrauma recursive: true); } } - if (character.Submarine != null) + if (character.Submarine != null && targetCharacter.CurrentHull != null) { if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { // Incapacitated target is not in a safe place -> Move to a safe place first if (character.SelectedCharacter != targetCharacter) { - if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", ("[targetname]", targetCharacter.Name, FormatCapitals.No), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 20be4494f..50f03a240 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -71,11 +71,11 @@ namespace Barotrauma { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; - if (affliction.Prefab.AfflictionType == "paralysis") + if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { vitality -= affliction.Strength; } - else if (affliction.Prefab.AfflictionType == "poison") + else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) { vitality -= affliction.Strength; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index c96c97b8c..14fc0d495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -12,6 +12,9 @@ namespace Barotrauma private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; private bool usingEscapeBehavior, isSteeringThroughGap; + public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 042e913cd..0b48320e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -465,6 +465,10 @@ namespace Barotrauma public readonly OrderTarget TargetPosition; private ISpatialEntity targetSpatialEntity; + + /// + /// Note this property doesn't return the follow target of the Follow objective, as expected! + /// public ISpatialEntity TargetSpatialEntity { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 9e9398673..88db60899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -348,6 +348,7 @@ namespace Barotrauma { if (body.UserData is Submarine) { return false; } if (body.UserData is Structure s && !s.IsPlatform) { return false; } + if (body.UserData is Voronoi2.VoronoiCell) { return false; } if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index b1a83f852..874a081bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -47,7 +47,8 @@ namespace Barotrauma public void SetOrder(Character orderedCharacter) { OrderedCharacter = orderedCharacter; - if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option))) + if (OrderedCharacter.AIController is HumanAIController humanAI && + humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem)) { if (orderedCharacter != CommandingCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index d4d1ad1ad..402d3ed94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -17,7 +17,20 @@ namespace Barotrauma float GetTargetingImportance(Entity entity) { float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition); - return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance); + + float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f); + if (TargetItem.Submarine != null) + { + Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition; + Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition; + if (Vector2.Dot(dir, submarineDir) < 0) + { + //direction from the weapon to the target is opposite to the direction from the sub to the weapon + // = the turret is most likely on the wrong side of the sub, reduce importance + importance *= 0.1f; + } + } + return importance; } public override void CalculateImportanceSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index b4a18d756..06be740df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -229,7 +229,7 @@ namespace Barotrauma #if DEBUG ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter); #endif - attendedIssues.Add(shipIssueWorker); + InsertIssue(shipIssueWorker, attendedIssues); } else { @@ -237,12 +237,19 @@ namespace Barotrauma ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to"); #endif shipIssueWorker.RemoveOrder(); - availableIssues.Add(shipIssueWorker); + InsertIssue(shipIssueWorker, availableIssues); } } - availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance)); - attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance)); + static void InsertIssue(ShipIssueWorker issue, List list) + { + int index = 0; + while (index < list.Count && list[index].Importance > issue.Importance) + { + index++; + } + list.Insert(index, issue); + } ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs index 7b2abeada..bbd554491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs @@ -13,6 +13,7 @@ namespace Barotrauma private readonly float minDistFromClosest; private readonly float maxDistFromCenter; private readonly float cohesion; + public bool ForceActive { get; private set; } public List Members { get; private set; } = new List(); public HashSet ActiveMembers { get; private set; } = new HashSet(); @@ -26,9 +27,10 @@ namespace Barotrauma public SwarmBehavior(XElement element, EnemyAIController ai) { this.ai = ai; - minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f)); - maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f)); - cohesion = element.GetAttributeFloat("cohesion", 1) / 10; + minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(minDistFromClosest), 10.0f)); + maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(maxDistFromCenter), 1000.0f)); + cohesion = element.GetAttributeFloat(nameof(cohesion), 1) / 10; + ForceActive = element.GetAttributeBool(nameof(ForceActive), false); } public static void CreateSwarm(IEnumerable swarm) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index b50108c6d..34092a282 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -8,16 +8,90 @@ using System; namespace Barotrauma { - partial class WreckAI : IServerSerializable + internal class SubmarineTurretAI { - public Submarine Wreck { get; private set; } + public Submarine Submarine { get; protected set; } + protected readonly List turrets = new List(); + public Identifier FriendlyTag; + public SubmarineTurretAI(Submarine submarine, Identifier friendlyTag = default) + { + FriendlyTag = friendlyTag; + Submarine = submarine; + foreach (Item item in Item.ItemList) + { + if (item.Submarine != Submarine) { continue; } + var turret = item.GetComponent(); + if (turret != null) + { + turrets.Add(turret); + // Set false, because we manage the turrets in the Update method. + turret.AutoOperate = false; + // Set to full condition, because items don't work when they are broken. + turret.Item.Condition = turret.Item.MaxCondition; + foreach (MapEntity linkedEntity in turret.Item.linkedTo) + { + if (linkedEntity is Item linkedItem) + { + linkedItem.Condition = linkedItem.MaxCondition; + } + } + } + } + LoadAllTurrets(); + } + + public virtual void Update(float deltaTime) + { + if (Submarine == null || Submarine.Removed) { return; } + OperateTurrets(deltaTime, FriendlyTag); + } + + protected virtual void LoadAllTurrets() + { + foreach (var turret in turrets) + { + LoadTurret(turret); + } + } + + protected void LoadTurret(Turret turret, Func ammoFilter = null) + { + foreach (var linkedItem in turret.Item.GetLinkedEntities()) + { + var container = linkedItem.GetComponent(); + if (container == null) { continue; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + if (container.Inventory.GetItemAt(i) != null) { continue; } + if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && (ammoFilter == null || ammoFilter(ip)), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) + { + Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Submarine); + if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + turret.Item.Remove(); + } + } + } + } + } + + protected void OperateTurrets(float deltaTime, Identifier friendlyTag) + { + foreach (var turret in turrets) + { + turret.UpdateAutoOperate(deltaTime, friendlyTag); + } + } + } + + partial class WreckAI : SubmarineTurretAI, IServerSerializable + { public bool IsAlive { get; private set; } private readonly List allItems; private readonly List thalamusItems; private readonly List thalamusStructures; - private readonly List turrets = new List(); private readonly List wayPoints = new List(); private readonly List hulls = new List(); private readonly List spawnOrgans = new List(); @@ -25,7 +99,7 @@ namespace Barotrauma private bool initialCellsSpawned; - public readonly WreckAIConfig Config; + public WreckAIConfig Config { get; private set; } private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -44,15 +118,10 @@ namespace Barotrauma return wreckAI; } - private WreckAI(Submarine wreck) + private WreckAI(Submarine wreck) : base(wreck) { - Wreck = wreck; - Config = WreckAIConfig.GetRandom(); - if (Config == null) - { - DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); - return; - } + GetConfig(); + if (Config == null) { return; } var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient); if (brainPrefab == null) @@ -60,20 +129,20 @@ namespace Barotrauma DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); return; } - allItems = Wreck.GetItems(false); + allItems = wreck.GetItems(false); thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab)); - hulls.AddRange(Wreck.GetHulls(false)); + hulls.AddRange(wreck.GetHulls(false)); var potentialBrainHulls = new List<(Hull hull, float weight)>(); - brain = new Item(brainPrefab, Vector2.Zero, Wreck); + brain = new Item(brainPrefab, Vector2.Zero, wreck); thalamusItems.Add(brain); Point minSize = brain.Rect.Size.Multiply(brain.Scale); // Bigger hulls are allowed, but not preferred more than what's sufficent. Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f); // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. - Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height)); + Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height)); foreach (Hull hull in hulls) { - float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition); + float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition); float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter)); float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width)); float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height)); @@ -121,7 +190,7 @@ namespace Barotrauma var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); if (backgroundPrefab != null) { - new Structure(brainHull.Rect, backgroundPrefab, Wreck); + new Structure(brainHull.Rect, backgroundPrefab, wreck); } var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient); if (horizontalWallPrefab != null) @@ -129,8 +198,8 @@ namespace Barotrauma int height = (int)horizontalWallPrefab.Size.Y; int halfHeight = height / 2; int quarterHeight = halfHeight / 2; - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); } var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient); if (verticalWallPrefab != null) @@ -138,50 +207,13 @@ namespace Barotrauma int width = (int)verticalWallPrefab.Size.X; int halfWidth = width / 2; int quarterWidth = halfWidth / 2; - new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); - new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); + new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); } - foreach (Item item in allItems) + foreach (Item item in thalamusItems) { - if (thalamusItems.Contains(item)) - { - // Ensure that thalamus items are visible - item.HiddenInGame = false; - } - else - { - // Load regular turrets - var turret = item.GetComponent(); - if (turret != null) - { - foreach (var linkedItem in item.GetLinkedEntities()) - { - var container = linkedItem.GetComponent(); - if (container == null) { continue; } - for (int i = 0; i < container.Inventory.Capacity; i++) - { - if (container.Inventory.GetItemAt(i) != null) { continue; } - if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && - Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) - { - Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); - if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) - { - item.Remove(); - } - } - } - } - } - } - } - foreach (var item in allItems) - { - var turret = item.GetComponent(); - if (turret != null) - { - turrets.Add(turret); - } + // Ensure that thalamus items are visible + item.HiddenInGame = false; if (item.HasTag(Config.Spawner)) { if (!spawnOrgans.Contains(item)) @@ -195,16 +227,34 @@ namespace Barotrauma } } } - wayPoints.AddRange(Wreck.GetWaypoints(false)); + wayPoints.AddRange(wreck.GetWaypoints(false)); IsAlive = true; - thalamusStructures = GetThalamusEntities(Wreck, Config.Entity).ToList(); + thalamusStructures = GetThalamusEntities(wreck, Config.Entity).ToList(); + } + + private void GetConfig() + { + Config ??= WreckAIConfig.GetRandom(); + if (Config == null) + { + DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); + } + } + + protected override void LoadAllTurrets() + { + GetConfig(); + foreach (var turret in turrets) + { + LoadTurret(turret, ip => Config.ForbiddenAmmunition.None(id => id == ip.Identifier)); + } } private readonly List destroyedOrgans = new List(); - public void Update(float deltaTime) + public override void Update(float deltaTime) { if (!IsAlive) { return; } - if (Wreck == null || Wreck.Removed) + if (Submarine == null || Submarine.Removed) { Remove(); return; @@ -223,34 +273,60 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); - bool someoneNearby = false; + bool isSomeoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; - foreach (Submarine submarine in Submarine.Loaded) +#if SERVER + foreach (var client in GameMain.Server.ConnectedClients) { - if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + var spectatePos = client.SpectatePos; + if (spectatePos.HasValue) { - someoneNearby = true; - break; + if (IsCloseEnough(spectatePos.Value, minDist)) + { + isSomeoneNearby = true; + break; + } } } - foreach (Character c in Character.CharacterList) +#else + if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist)) { - if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } - if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + isSomeoneNearby = true; + } +#endif + if (!isSomeoneNearby) + { + foreach (Submarine submarine in Submarine.Loaded) { - someoneNearby = true; - break; + if (submarine.Info.Type != SubmarineType.Player) { continue; } + if (IsCloseEnough(submarine.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } } } - if (!someoneNearby) { return; } - OperateTurrets(deltaTime); + if (!isSomeoneNearby) + { + foreach (Character c in Character.CharacterList) + { + if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; } + if (IsCloseEnough(c.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } + } + } + if (!isSomeoneNearby) { return; } + OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { if (!initialCellsSpawned) { SpawnInitialCells(); } UpdateReinforcements(deltaTime); } } + private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist; private void SpawnInitialCells() { @@ -287,7 +363,7 @@ namespace Barotrauma // Snap all tendons foreach (Item item in turret.ActiveProjectiles) { - if (item.GetComponent()?.IsStuckToTarget ?? false) + if (item.GetComponent() is { IsStuckToTarget: true }) { item.Condition = 0; } @@ -314,7 +390,7 @@ namespace Barotrauma { // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. float maxDistance = Sonar.DefaultSonarRange; - if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance) + if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance) { character.Kill(CauseOfDeathType.Unknown, null); } @@ -333,7 +409,7 @@ namespace Barotrauma public void Remove() { Kill(); - RemoveThalamusItems(Wreck); + RemoveThalamusItems(Submarine); thalamusItems?.Clear(); thalamusStructures?.Clear(); } @@ -387,7 +463,7 @@ namespace Barotrauma return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t)); } - void UpdateReinforcements(float deltaTime) + private void UpdateReinforcements(float deltaTime) { if (spawnOrgans.Count == 0) { return; } cellSpawnTimer -= deltaTime; @@ -398,7 +474,7 @@ namespace Barotrauma } } - bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) + private bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) { cell = null; if (protectiveCells.Count >= MaxCellCount) { return false; } @@ -424,19 +500,6 @@ namespace Barotrauma cellSpawnTimer = GetSpawnTime(); return true; } - - void OperateTurrets(float deltaTime) - { - foreach (var turret in turrets) - { - // Never target other creatures than humans with the turrets. - turret.ThalamusOperate(this, deltaTime, - !turret.Item.HasTag("ignorecharacters"), - targetOtherCreatures: false, - !turret.Item.HasTag("ignoresubmarines"), - turret.Item.HasTag("ignoreaimdelay")); - } - } void OnCellDeath(Character character, CauseOfDeath causeOfDeath) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 140aa7c04..1606de078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -541,11 +541,11 @@ namespace Barotrauma float wobbleStrength = 0.0f; if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType); } if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType); } if (wobbleStrength <= 0.1f) { return 0.0f; } wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 4e3da61fa..6d726f848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -135,8 +135,14 @@ namespace Barotrauma public override void UpdateAnim(float deltaTime) { - if (Frozen) return; - if (MainLimb == null) { return; } + //wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate + if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; } + if (Frozen) { return; } + if (MainLimb == null) + { + ResetState(); + return; + } var mainLimb = MainLimb; levitatingCollider = !IsHanging; @@ -164,6 +170,7 @@ namespace Barotrauma //cannot walk but on dry land -> wiggle around UpdateDying(deltaTime); } + ResetState(); return; } else @@ -176,11 +183,17 @@ namespace Barotrauma { var lowestLimb = FindLowestLimb(); - Collider.SetTransform(new Vector2( - Collider.SimPosition.X, - Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), - 0.0f); - + if (InWater) + { + Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f); + } + else + { + Collider.SetTransform(new Vector2( + Collider.SimPosition.X, + Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), + 0.0f); + } Collider.Enabled = true; } @@ -223,6 +236,7 @@ namespace Barotrauma if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); + ResetState(); return; } if (character.AnimController.AnimationTestPose) @@ -230,7 +244,11 @@ namespace Barotrauma ApplyTestPose(); } //don't flip when simply physics is enabled - if (SimplePhysicsEnabled) { return; } + if (SimplePhysicsEnabled) + { + ResetState(); + return; + } if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming) { @@ -264,43 +282,47 @@ namespace Barotrauma } } - if (!CurrentFishAnimation.Flip) { return; } - if (IsStuck) { return; } - if (character.AIController != null && !character.AIController.CanFlip) { return; } - - flipCooldown -= deltaTime; - if (TargetDir != Direction.None && TargetDir != dir) + if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false }) { - flipTimer += deltaTime; - // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). - float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; - if (CurrentHull != null) + flipCooldown -= deltaTime; + if (TargetDir != Direction.None && TargetDir != dir) { - // Enemy movement speeds are halved inside submarines - requiredSpeed /= 2; - } - bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; - bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); - if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) - || character.IsRemotePlayer) - { - Flip(); - if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) + flipTimer += deltaTime; + // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). + float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; + if (CurrentHull != null) { - Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); + // Enemy movement speeds are halved inside submarines + requiredSpeed /= 2; } + bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; + bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); + if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) + || character.IsRemotePlayer) + { + Flip(); + if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) + { + Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); + } + flipTimer = 0.0f; + flipCooldown = CurrentFishAnimation.FlipCooldown; + } + } + else + { flipTimer = 0.0f; - flipCooldown = CurrentFishAnimation.FlipCooldown; } } - else + ResetState(); + + void ResetState() { - flipTimer = 0.0f; + wasAiming = aiming; + aiming = false; + wasAimingMelee = aimingMelee; + aimingMelee = false; } - wasAiming = aiming; - aiming = false; - wasAimingMelee = aimingMelee; - aimingMelee = false; } private bool CanDrag(Character target) @@ -458,24 +480,34 @@ namespace Barotrauma t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + if (Collider.BodyType == BodyType.Dynamic) + { + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; - if (aiming && movement.Length() <= 0.1f) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } - - if (!isMoving) + if (!isMoving && !CurrentSwimParams.UpdateAnimationWhenNotMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); mainLimb.PullJointWorldAnchorB = Collider.SimPosition; + if (aiming) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; + TargetMovement = new Vector2(0.0f, -0.1f); + float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver2 * Dir); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier * 2); + if (TorsoAngle.HasValue) + { + Limb torso = GetLimb(LimbType.Torso); + if (torso != null) + { + SmoothRotateWithoutWrapping(torso, newRotation + TorsoAngle.Value * Dir, mainLimb, TorsoTorque * 2); + } + } + } } else { @@ -688,9 +720,12 @@ namespace Barotrauma { movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); - Collider.LinearVelocity = new Vector2( - movement.X, - Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); + if (Collider.BodyType == BodyType.Dynamic) + { + Collider.LinearVelocity = new Vector2( + movement.X, + Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); + } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index bb4f57c72..f78151840 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -170,7 +170,7 @@ namespace Barotrauma { get { - float shoulderHeight = Collider.height / 2.0f; + float shoulderHeight = Collider.Height / 2.0f; if (inWater) { shoulderHeight += 0.4f; @@ -308,7 +308,7 @@ namespace Barotrauma Collider.SetTransform(new Vector2( Collider.SimPosition.X, - Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), + Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), Collider.Rotation); Collider.FarseerBody.ResetDynamics(); @@ -459,7 +459,8 @@ namespace Barotrauma void UpdateStanding() { - if (CurrentGroundedParams == null) { return; } + var currentGroundedParams = CurrentGroundedParams; + if (currentGroundedParams == null) { return; } Vector2 handPos; Limb leftFoot = GetLimb(LimbType.LeftFoot); @@ -482,7 +483,7 @@ namespace Barotrauma walkCycleMultiplier *= 1.5f; } - float getUpForce = CurrentGroundedParams.GetUpForce / RagdollParams.JointScale; + float getUpForce = currentGroundedParams.GetUpForce / RagdollParams.JointScale; Vector2 colliderPos = GetColliderBottom(); if (Math.Abs(TargetMovement.X) > 1.0f) @@ -583,7 +584,7 @@ namespace Barotrauma } float stepLift = TargetMovement.X == 0.0f ? 0 : - (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + (float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; @@ -598,7 +599,7 @@ namespace Barotrauma if (!head.Disabled) { - y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; + y = colliderPos.Y + stepLift * currentGroundedParams.StepLiftHeadMultiplier; if (HeadPosition.HasValue) { y += HeadPosition.Value; } if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } head.PullJointWorldAnchorB = @@ -615,18 +616,18 @@ namespace Barotrauma if (TorsoAngle.HasValue && !torso.Disabled) { float torsoAngle = TorsoAngle.Value; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; - torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); + torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque); } if (!head.Disabled) { - if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!Aiming && currentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } - head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); + head.body.SmoothRotate(headAngle * Dir, currentGroundedParams.HeadTorque); } else { @@ -665,16 +666,16 @@ namespace Barotrauma if (footPos.Y < 0.0f) { footPos.Y = -0.15f; } //make the character limp if the feet are damaged - float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true); + float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.DamageType, foot, true); footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f)); - if (CurrentGroundedParams.FootLiftHorizontalFactor > 0) + if (currentGroundedParams.FootLiftHorizontalFactor > 0) { // Calculate the foot y dynamically based on the foot position relative to the waist, // so that the foot aims higher when it's behind the waist and lower when it's in the front. float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir; - float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor); - float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor); + float min = MathUtils.InverseLerp(1, 0, currentGroundedParams.FootLiftHorizontalFactor); + float max = 1 + MathUtils.InverseLerp(0, 1, currentGroundedParams.FootLiftHorizontalFactor); float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff)); footPos.Y *= xFactor; } @@ -698,19 +699,19 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = colliderPos + footPos; - MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength); + MoveLimb(foot, colliderPos + footPos, currentGroundedParams.FootMoveStrength); FootIK(foot, colliderPos + footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + currentGroundedParams.LegBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } //calculate the positions of hands handPos = torso.SimPosition; - handPos.X = -walkPosX * CurrentGroundedParams.HandMoveAmount.X; + handPos.X = -walkPosX * currentGroundedParams.HandMoveAmount.X; - float lowerY = CurrentGroundedParams.HandClampY; + float lowerY = currentGroundedParams.HandClampY; - handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * CurrentGroundedParams.HandMoveAmount.Y)); + handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * currentGroundedParams.HandMoveAmount.Y)); Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y); @@ -718,13 +719,13 @@ namespace Barotrauma { HandIK(rightHand, torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } if (leftHand != null && !leftHand.Disabled) { HandIK(leftHand, torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } } else @@ -755,8 +756,8 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = footPos; - float footMoveForce = CurrentGroundedParams.FootMoveStrength; - float legBendTorque = CurrentGroundedParams.LegBendTorque; + float footMoveForce = currentGroundedParams.FootMoveStrength; + float legBendTorque = currentGroundedParams.LegBendTorque; if (Crouching) { // Keeps the pose @@ -764,7 +765,7 @@ namespace Barotrauma footMoveForce *= 2; } MoveLimb(foot, footPos, footMoveForce); - FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + FootIK(foot, footPos, legBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } @@ -780,7 +781,7 @@ namespace Barotrauma var arm = GetLimb(armType); if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f) { - arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength); + arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * currentGroundedParams.ArmMoveStrength); } //get the elbow to a neutral rotation @@ -791,14 +792,14 @@ namespace Barotrauma if (elbow != null) { float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit); - forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength); + forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * currentGroundedParams.ArmMoveStrength); } } // Try to keep the wrist straight LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type); if (wrist != null) { - hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength); + hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * currentGroundedParams.HandMoveStrength); } } } @@ -840,14 +841,11 @@ namespace Barotrauma if (head == null) { return; } if (torso == null) { return; } - const float DisableMovementAboveSurfaceThreshold = 50.0f; - if (currentHull != null && character.CurrentHull != null) { float surfacePos = GetSurfaceY(); float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos); - if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; } } Limb leftHand = GetLimb(LimbType.LeftHand); @@ -917,6 +915,7 @@ namespace Barotrauma RotateHead(head); } + const float DisableMovementAboveSurfaceThreshold = 50.0f; //dont try to move upwards if head is already out of water if (surfaceLimiter > 1.0f && TargetMovement.Y > 0.0f) { @@ -936,8 +935,8 @@ namespace Barotrauma //turn head above the water head.body.ApplyTorque(Dir); } + movement.Y *= Math.Max(0, 1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); - movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); } bool isNotRemote = true; @@ -956,7 +955,13 @@ namespace Barotrauma t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + Vector2 targetVelocity = movement; + //if we're too high above the surface, don't touch the vertical velocity of the collider unless we're heading down + if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) + { + targetVelocity.Y = Math.Min(Collider.LinearVelocity.Y, movement.Y); + }; + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, targetVelocity, t); } WalkPos += movement.Length(); @@ -1130,7 +1135,7 @@ namespace Barotrauma ladderSimPos -= currentHull.Submarine.SimPosition; } - float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f; + float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f; float torsoPos = TorsoPosition ?? 0; MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f); float headPos = HeadPosition ?? 0; @@ -1225,7 +1230,7 @@ namespace Barotrauma if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); } //reached the bottom -> can't go further down - float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.height; + float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height; if (floorFixture != null && !floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) && !floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) && @@ -1524,13 +1529,15 @@ namespace Barotrauma Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); - Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); - if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); } - if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; } + Limb targetLeftHand = + target.AnimController.GetLimb(LimbType.LeftForearm) ?? + target.AnimController.GetLimb(LimbType.Torso) ?? + target.AnimController.MainLimb; - Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); - if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); } - if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; } + Limb targetRightHand = + target.AnimController.GetLimb(LimbType.RightForearm) ?? + target.AnimController.GetLimb(LimbType.Torso) ?? + target.AnimController.MainLimb; if (!target.AllowInput) { @@ -1546,10 +1553,7 @@ namespace Barotrauma return; } Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); - if (targetTorso == null) - { - targetTorso = target.AnimController.MainLimb; - } + targetTorso ??= target.AnimController.MainLimb; if (target.AnimController.Dir != Dir) { target.AnimController.Flip(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index b5e116dab..cc28d59c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -173,18 +173,18 @@ namespace Barotrauma if (value == colliderIndex || collider == null) { return; } if (value >= collider.Count || value < 0) { return; } - if (collider[colliderIndex].height < collider[value].height) + if (collider[colliderIndex].Height < collider[value].Height) { Vector2 pos1 = collider[colliderIndex].SimPosition; - pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor; + pos1.Y -= collider[colliderIndex].Height * ColliderHeightFromFloor; Vector2 pos2 = pos1; - pos2.Y += collider[value].height * 1.1f; + pos2.Y += collider[value].Height * 1.1f; if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; } } Vector2 pos = collider[colliderIndex].SimPosition; - pos.Y -= collider[colliderIndex].height * 0.5f; - pos.Y += collider[value].height * 0.5f; + pos.Y -= collider[colliderIndex].Height * 0.5f; + pos.Y += collider[value].Height * 0.5f; collider[value].SetTransform(pos, collider[colliderIndex].Rotation); collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity; @@ -575,6 +575,10 @@ namespace Barotrauma protected void AddLimb(LimbParams limbParams) { + if (limbParams.ID < 0 || limbParams.ID > 255) + { + throw new Exception($"Invalid limb params in limb \"{limbParams.Type}\". \"{limbParams.ID}\" is not a valid limb ID."); + } byte ID = Convert.ToByte(limbParams.ID); Limb limb = new Limb(this, character, limbParams); limb.body.FarseerBody.OnCollision += OnLimbCollision; @@ -680,6 +684,10 @@ namespace Barotrauma } return true; } + else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine) + { + return false; + } Vector2 colliderBottom = GetColliderBottom(); if (structure.IsPlatform) @@ -1189,7 +1197,7 @@ namespace Barotrauma { inWater = false; headInWater = false; - RefreshFloorY(ignoreStairs: Stairs == null); + RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); } //ragdoll isn't in any room -> it's in the water else if (currentHull == null) @@ -1201,10 +1209,12 @@ namespace Barotrauma { headInWater = false; inWater = false; - RefreshFloorY(ignoreStairs: Stairs == null); + RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); if (currentHull.WaterPercentage > 0.001f) { - float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY()); + (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY(); + float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits); + float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits); if (targetMovement.Y < 0.0f) { Vector2 colliderBottom = GetColliderBottom(); @@ -1214,13 +1224,21 @@ namespace Barotrauma { //set floorY to the position of the floor in the hull below the character var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false); - if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + if (lowerHull != null) + { + floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + } } } float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f; - if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f) + if (Collider.SimPosition.Y < waterSurfaceY) { - inWater = true; + //too deep to stand up, or not enough room to stand up + if (waterSurfaceY - floorY > standHeight * 0.8f || + ceilingY - floorY < standHeight * 0.8f) + { + inWater = true; + } } } } @@ -1281,21 +1299,32 @@ namespace Barotrauma limb.Update(deltaTime); } - if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround) + if (!inWater && character.AllowInput && levitatingCollider) { - 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 && onGround) + if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { - if (Stairs != null) + 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) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, - (targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f); + if (Stairs != null) + { + 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); + } } - else + } + else + { + // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. + if (Collider.LinearVelocity == Vector2.Zero) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f); + character.IsRagdolled = true; } - } + } } UpdateProjSpecific(deltaTime, cam); forceNotStanding = false; @@ -1525,15 +1554,24 @@ namespace Barotrauma lastFloorCheckPos = Vector2.Zero; } - private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false) + // 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) { + floorYCheckTimer -= deltaTime; PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; - if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms) + if (floorYCheckTimer < 0 || + lastFloorCheckIgnoreStairs != ignoreStairs || + lastFloorCheckIgnorePlatforms != IgnorePlatforms || + Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f) { floorY = GetFloorY(refBody.SimPosition, ignoreStairs); lastFloorCheckPos = refBody.SimPosition; lastFloorCheckIgnoreStairs = ignoreStairs; lastFloorCheckIgnorePlatforms = IgnorePlatforms; + // Add some randomness to prevent all stationary characters doing the checks at the same frame. + floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f); } } @@ -1616,7 +1654,7 @@ namespace Barotrauma { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; - if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f) + if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f) { onGround = true; if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs) @@ -1655,22 +1693,34 @@ namespace Barotrauma } } + /// + /// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in) + /// public float GetSurfaceY() + { + return GetWaterSurfaceAndCeilingY().WaterSurfaceY; + } + + /// + /// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in). + /// + private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY() { //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside if (currentHull == null || character.CurrentHull == null) { - return float.PositiveInfinity; + return (float.PositiveInfinity, float.PositiveInfinity); } - - float surfacePos = currentHull.Surface; + + float surfaceY = currentHull.Surface; + float ceilingY = currentHull.Rect.Y; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); //if the hull is almost full of water, check if there's a water-filled hull above it //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) - { - GetSurfacePos(currentHull, ref surfacePos); - void GetSurfacePos(Hull hull, ref float prevSurfacePos) + { + GetSurfacePos(currentHull, ref surfaceY, ref ceilingY); + void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos) { if (prevSurfacePos > surfaceThreshold) { return; } foreach (Gap gap in hull.ConnectedGaps) @@ -1681,6 +1731,7 @@ namespace Barotrauma //if the gap is above us and leads outside, there's no surface to limit the movement if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) { + ceilingPos += 100000.0f; prevSurfacePos += 100000.0f; return; } @@ -1689,15 +1740,16 @@ namespace Barotrauma { if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { - prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); - GetSurfacePos(otherHull, ref prevSurfacePos); + prevSurfacePos = Math.Max(surfaceY, otherHull.Surface); + ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y); + GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos); break; } } } } } - return surfacePos; + return (surfaceY, ceilingY); } public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) @@ -1803,22 +1855,36 @@ namespace Barotrauma private bool collisionsDisabled; + private double lastObstacleRayCastTime; protected void CheckDistFromCollider() { - float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; + float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f; allowedDist = Math.Max(allowedDist, 1.0f); float resetDist = allowedDist * 5.0f; + float obstacleCheckDist = 0.3f; + Vector2 diff = Collider.SimPosition - MainLimb.SimPosition; float distSqrd = diff.LengthSquared(); - if (distSqrd > resetDist * resetDist) + bool shouldReset = distSqrd > resetDist * resetDist; + if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist) + { + if (Timing.TotalTime > lastObstacleRayCastTime + 1 && + Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null) + { + shouldReset = true; + lastObstacleRayCastTime = Timing.TotalTime; + } + } + + if (shouldReset) { //ragdoll way too far, reset position SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true); } - if (distSqrd > allowedDist * allowedDist) + else if (distSqrd > allowedDist * allowedDist) { //ragdoll too far from the collider, disable collisions until it's close enough //(in case the ragdoll has gotten stuck somewhere) @@ -1840,7 +1906,7 @@ namespace Barotrauma collisionsDisabled = false; //force collision categories to be updated prevCollisionCategory = Category.None; - } + } } partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 702512ca4..2a9aa8211 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -181,13 +181,13 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool Ranged { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")] + [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, IsPropertySaveable.Yes)] + [Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable] public float RequiredAngle { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable] @@ -199,6 +199,12 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable] public int RotationLimbIndex { get; set; } + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items). Default 0 means the weapon is not swayed at all."), Editable] + public float SwayAmount { get; set; } + + [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] + public float SwayFrequency { get; set; } + /// /// Legacy support. Use Afflictions. /// @@ -337,9 +343,10 @@ namespace Barotrauma return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime; } - public float GetItemDamage(float deltaTime) + public float GetItemDamage(float deltaTime, float multiplier = 1) { - return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime; + float dmg = ItemDamage * multiplier; + return (Duration == 0.0f) ? dmg : dmg * deltaTime; } public float GetTotalDamage(bool includeStructureDamage = false) @@ -421,13 +428,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - Conditionals.Add(new PropertyConditional(attribute)); - } - } + Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 5ee296c48..a03315f66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -136,6 +136,12 @@ namespace Barotrauma public bool IsEscorted { get; set; } public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty; + public bool DoesBleed + { + get => Params.Health.DoesBleed; + set => Params.Health.DoesBleed = value; + } + public readonly Dictionary Properties; public Dictionary SerializableProperties { @@ -173,6 +179,13 @@ namespace Barotrauma } } + private Identifier? faction; + public Identifier Faction + { + get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; } + set { faction = value; } + } + private CharacterTeamType teamID; public CharacterTeamType TeamID { @@ -184,6 +197,13 @@ namespace Barotrauma } } + + private CharacterTeamType? originalTeamID; + public CharacterTeamType OriginalTeamID + { + get { return originalTeamID ?? teamID; } + } + private Wallet wallet; public Wallet Wallet @@ -205,7 +225,7 @@ namespace Barotrauma protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; - const string OriginalTeamIdentifier = "original"; + private const string OriginalChangeTeamIdentifier = "original"; private void ThrowIfAccessingWalletsInSingleplayer() { @@ -220,20 +240,16 @@ namespace Barotrauma public void SetOriginalTeam(CharacterTeamType newTeam) { - TryRemoveTeamChange(OriginalTeamIdentifier); + TryRemoveTeamChange(OriginalChangeTeamIdentifier); currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base); - TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange); + TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange); } - protected void ChangeTeam(CharacterTeamType newTeam) + private void ChangeTeam(CharacterTeamType newTeam) { - if (newTeam == teamID) - { - return; - } - teamID = newTeam; - if (info != null) { info.TeamID = newTeam; } - + if (newTeam == teamID) { return; } + if (originalTeamID == null) { originalTeamID = teamID; } + TeamID = newTeam; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -277,7 +293,7 @@ namespace Barotrauma { if (currentTeamChange == removedTeamChange) { - currentTeamChange = activeTeamChanges[OriginalTeamIdentifier]; + currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier]; } } return activeTeamChanges.Remove(identifier); @@ -311,7 +327,9 @@ namespace Barotrauma } } - public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; + public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2; + + public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2; public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; public CombatAction CombatAction; @@ -360,7 +378,7 @@ namespace Barotrauma public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); - public Identifier Group => Params.Group; + public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; public bool IsHumanoid => Params.Humanoid; @@ -458,10 +476,15 @@ namespace Barotrauma } set { - if (info != null && info != value) info.Remove(); - + if (info != null && info != value) + { + info.Remove(); + } info = value; - if (info != null) info.Character = this; + if (info != null) + { + info.Character = this; + } } } @@ -521,8 +544,13 @@ namespace Barotrauma } set { + bool wasHidden = HideFace; hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); - if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true); + bool isHidden = HideFace; + if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden) + { + info.CheckDisguiseStatus(true); + } } } @@ -728,7 +756,7 @@ namespace Barotrauma /// public bool InPressure { - get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; } + get { return CurrentHull == null || CurrentHull.LethalPressure > 0.0f; } } /// @@ -752,7 +780,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength); } } @@ -836,7 +864,7 @@ namespace Barotrauma public float Bleeding { - get { return CharacterHealth.GetAfflictionStrength("bleeding", true); } + get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); } } private bool speechImpedimentSet; @@ -1041,7 +1069,7 @@ namespace Barotrauma public bool InWater => AnimController is AnimController { InWater: true }; - public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold; + public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100; public bool GodMode = false; @@ -1099,6 +1127,12 @@ namespace Barotrauma public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; + public float AITurretPriority + { + get => Params.AITurretPriority; + private set => Params.AITurretPriority = value; + } + public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); public OnDeathHandler OnDeath; @@ -1619,7 +1653,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); } - else if (humanPrefab.GiveItems(this, Submarine)) + else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint)) { return; } @@ -1691,7 +1725,7 @@ namespace Barotrauma if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) { skillLevel += skillValue; - break; + break; } } @@ -1700,9 +1734,7 @@ namespace Barotrauma } skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); - - - return skillLevel; + return Math.Max(skillLevel, 0); } // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner @@ -1791,20 +1823,8 @@ namespace Barotrauma public void StackSpeedMultiplier(float val) { - if (val < 1f) - { - if (val < greatestNegativeSpeedMultiplier) - { - greatestNegativeSpeedMultiplier = val; - } - } - else - { - if (val > greatestPositiveSpeedMultiplier) - { - greatestPositiveSpeedMultiplier = val; - } - } + greatestNegativeSpeedMultiplier = Math.Min(val, greatestNegativeSpeedMultiplier); + greatestPositiveSpeedMultiplier = Math.Max(val, greatestPositiveSpeedMultiplier); } public void ResetSpeedMultiplier() @@ -1827,20 +1847,8 @@ namespace Barotrauma public void StackHealthMultiplier(float val) { - if (val < 1f) - { - if (val < greatestNegativeHealthMultiplier) - { - greatestNegativeHealthMultiplier = val; - } - } - else - { - if (val > greatestPositiveHealthMultiplier) - { - greatestPositiveHealthMultiplier = val; - } - } + greatestNegativeHealthMultiplier = Math.Min(val, greatestNegativeHealthMultiplier); + greatestPositiveHealthMultiplier = Math.Max(val, greatestPositiveHealthMultiplier); } private void CalculateHealthMultiplier() @@ -1900,7 +1908,7 @@ namespace Barotrauma { if (limb != null) { - sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage")); + sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType)); } return Math.Clamp(sum, 0, 1f); } @@ -2196,24 +2204,7 @@ namespace Barotrauma if (SelectedItem != null) { - if (IsKeyDown(InputType.Aim) || !SelectedItem.RequireAimToSecondaryUse) - { - SelectedItem.SecondaryUse(deltaTime, this); - } - if (IsKeyDown(InputType.Use) && SelectedItem != null && !SelectedItem.IsShootable) - { - if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - SelectedItem.Use(deltaTime, this); - } - } - if (IsKeyDown(InputType.Shoot) && SelectedItem != null && SelectedItem.IsShootable) - { - if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - SelectedItem.Use(deltaTime, this); - } - } + tryUseItem(SelectedItem, deltaTime); } if (SelectedCharacter != null) @@ -2695,7 +2686,7 @@ namespace Barotrauma //character is outside but cursor position inside if (cursorPosition.Y > Level.Loaded.Size.Y) { - var sub = Submarine.FindContaining(cursorPosition); + var sub = Submarine.FindContainingInLocalCoordinates(cursorPosition); if (sub != null) cursorPosition += sub.Position; } } @@ -2872,7 +2863,7 @@ namespace Barotrauma } #endif } - else + else if (!IsClimbing) { #if CLIENT if (Controlled == this) @@ -2920,9 +2911,9 @@ namespace Barotrauma CharacterHealth.OpenHealthWindow = null; #endif } - else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null)) + else if (IsKeyHit(InputType.Health) && SelectedItem != null) { - SelectedItem = SelectedSecondaryItem = null; + SelectedItem = null; } else if (focusedItem != null) { @@ -3017,7 +3008,9 @@ namespace Barotrauma for (int i = 0; i < CharacterList.Count; i++) { - CharacterList[i].Update(deltaTime, cam); + var character = CharacterList[i]; + System.Diagnostics.Debug.Assert(character != null && !character.Removed); + character.Update(deltaTime, cam); } } @@ -3108,8 +3101,7 @@ namespace Barotrauma if (NeedsAir) { //implode if not protected from pressure, and either outside or in a high-pressure hull - if (!IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { if (CharacterHealth.PressureKillDelay <= 0.0f) { @@ -3136,15 +3128,17 @@ namespace Barotrauma PressureTimer = 0.0f; } } - else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && - PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) && - WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure)) + else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure) { - //implode if below crush depth, and either outside or in a high-pressure hull - if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f; + if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth) { - Implode(); - if (IsDead) { return; } + //implode if below crush depth, and either outside or in a high-pressure hull + if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + { + Implode(); + if (IsDead) { return; } + } } } @@ -4064,17 +4058,7 @@ namespace Barotrauma CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill); if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; } - CreatureMetrics.Instance.Killed.Add(target.SpeciesName); - AddEncounter(target); - } - - public void AddEncounter(Character other) - { - if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; } - CreatureMetrics.Instance.Encountered.Add(other.SpeciesName); - CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName); + CreatureMetrics.RecordKill(target.SpeciesName); } public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) @@ -4109,13 +4093,6 @@ namespace Barotrauma } } -#if CLIENT - if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) - { - CharacterHUD.ShowBossHealthBar(this); - } -#endif - Vector2 dir = hitLimb.WorldPosition - worldPosition; if (Math.Abs(attackImpulse) > 0.0f) { @@ -4157,13 +4134,24 @@ namespace Barotrauma if (attacker != null && attacker != this && !attacker.Removed) { AddAttacker(attacker, attackResult.Damage); - AddEncounter(attacker); - attacker.AddEncounter(this); + if (IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(attacker.SpeciesName); + } + if (attacker.IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(SpeciesName); + } } ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } - +#if CLIENT + if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) + { + CharacterHUD.ShowBossHealthBar(this, attackResult.Damage); + } +#endif return attackResult; } @@ -4181,7 +4169,8 @@ namespace Barotrauma { if (affliction.Prefab.IsBuff) { continue; } if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } - if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || + affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { if (!Params.Health.PoisonImmunity) { @@ -4261,7 +4250,7 @@ namespace Barotrauma if (Screen.Selected != GameMain.GameScreen) { return; } if (newStun > 0 && Params.Health.StunImmunity) { - if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -4288,7 +4277,7 @@ namespace Barotrauma float eatingRegen = Params.Health.HealthRegenerationWhenEating; if (eatingRegen > 0) { - CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime); + CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.DamageType, eatingRegen * deltaTime); } } if (statusEffects.TryGetValue(actionType, out var statusEffectList)) @@ -4298,7 +4287,7 @@ namespace Barotrauma if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.OnlyWhenDamagedByPlayer) { if (LastAttacker == null || !LastAttacker.IsPlayer) { @@ -4356,6 +4345,10 @@ namespace Barotrauma { statusEffect.Apply(actionType, deltaTime, this, this); } + if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null) + { + statusEffect.Apply(actionType, deltaTime, this, CurrentHull); + } } if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { @@ -4494,9 +4487,12 @@ namespace Barotrauma OnDeath?.Invoke(this, CauseOfDeath); - var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); - CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); - CauseOfDeath.Killer?.RecordKill(this); + if (CauseOfDeath.Type != CauseOfDeathType.Disconnected) + { + var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); + CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); + CauseOfDeath.Killer?.RecordKill(this); + } if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { @@ -4517,6 +4513,9 @@ namespace Barotrauma { foreach (Item heldItem in HeldItems.ToList()) { + //if the item is both wearable and holdable, and currently worn, don't drop the item + var wearable = heldItem.GetComponent(); + if (wearable is { IsActive: true }) { continue; } heldItem.Drop(this); } } @@ -4563,6 +4562,11 @@ namespace Barotrauma SetStun(0.0f, true); isDead = false; + if (info != null) + { + info.CauseOfDeath = null; + } + foreach (LimbJoint joint in AnimController.LimbJoints) { var revoluteJoint = joint.revoluteJoint; @@ -4586,10 +4590,7 @@ namespace Barotrauma limb.IsSevered = false; } - if (GameMain.GameSession != null) - { - GameMain.GameSession.ReviveCharacter(this); - } + GameMain.GameSession?.ReviveCharacter(this); } public override void Remove() @@ -4657,6 +4658,7 @@ namespace Barotrauma Submarine = null; AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false); AnimController.FindHull(worldPos, setSubmarine: true); + CurrentHull = AnimController.CurrentHull; if (AIController is HumanAIController humanAI) { humanAI.PathSteering?.ResetPath(); @@ -4707,7 +4709,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - newItem.CreateStatusEvent(); + newItem.CreateStatusEvent(loadingRound: true); } #if SERVER newItem.GetComponent()?.SyncHistory(); @@ -4903,34 +4905,36 @@ namespace Barotrauma return visibleHulls; } - public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos); + + public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null) { - Vector2 targetPos = target.SimPosition; + Vector2 targetPos = to.SimPosition; if (worldPos.HasValue) { Vector2 wp = worldPos.Value; - if (target.Submarine != null) + if (to.Submarine != null) { - wp -= target.Submarine.Position; + wp -= to.Submarine.Position; } targetPos = ConvertUnits.ToSimUnits(wp); } - if (Submarine == null && target.Submarine != null) + if (from.Submarine == null && to.Submarine != null) { // outside and targeting inside - targetPos += target.Submarine.SimPosition; + targetPos += to.Submarine.SimPosition; } - else if (Submarine != null && target.Submarine == null) + else if (from.Submarine != null && to.Submarine == null) { // inside and targeting outside - targetPos -= Submarine.SimPosition; + targetPos -= from.Submarine.SimPosition; } - else if (Submarine != target.Submarine) + else if (from.Submarine != to.Submarine) { - if (Submarine != null && target.Submarine != null) + if (from.Submarine != null && to.Submarine != null) { // both inside, but in different subs - Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition; + Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition; targetPos -= diff; } } @@ -4952,13 +4956,14 @@ namespace Barotrauma public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier; - public bool IsProtectedFromPressure() - { - return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - } + /// + /// Is the character currently protected from the pressure by immunity/ability or a status effect (e.g. from a diving suit). + /// + public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - // Talent logic begins here. Should be encapsulated to its own controller soon + public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure); + #region Talents private readonly List characterTalents = new List(); public void LoadTalents() @@ -5032,6 +5037,49 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + public bool HasUnlockedAllTalents() + { + if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (!talentOption.HasMaxTalents(info.UnlockedTalents)) + { + return false; + } + } + } + } + return true; + } + + public bool HasTalents() + { + return characterTalents.Any(); + } + + public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, abilityObject); + } + } + + public void CheckTalents(AbilityEffectType abilityEffectType) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, null); + } + } + + partial void OnTalentGiven(TalentPrefab talentPrefab); + + #endregion + private readonly HashSet sameRoomHulls = new(); /// @@ -5058,24 +5106,6 @@ namespace Barotrauma return sameRoomHulls.Contains(character.CurrentHull); } - public bool HasUnlockedAllTalents() - { - if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) - { - foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) - { - foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) - { - if (!talentOption.HasMaxTalents(info.UnlockedTalents)) - { - return false; - } - } - } - } - return true; - } - public static IEnumerable GetFriendlyCrew(Character character) { if (character is null) @@ -5085,27 +5115,6 @@ namespace Barotrauma return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); } - public bool HasTalents() - { - return characterTalents.Any(); - } - - public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, abilityObject); - } - } - - public void CheckTalents(AbilityEffectType abilityEffectType) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, null); - } - } - public bool HasRecipeForItem(Identifier recipeIdentifier) { return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); @@ -5169,7 +5178,6 @@ namespace Barotrauma #endif partial void OnMoneyChanged(int prevAmount, int newAmount); - partial void OnTalentGiven(TalentPrefab talentPrefab); /// /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. @@ -5345,7 +5353,7 @@ namespace Barotrauma public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); - public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 49c273024..9d0379987 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -11,6 +12,31 @@ using Barotrauma.Abilities; namespace Barotrauma { + [NetworkSerialize] + internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct + { + [return: MaybeNull] + public JobVariant ToJobVariant() + { + if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; } + return new JobVariant(jobPrefab, Variant); + } + + public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant); + } + + [NetworkSerialize(ArrayMaxSize = byte.MaxValue)] + internal readonly record struct NetCharacterInfo(string NewName, + ImmutableArray Tags, + byte HairIndex, + byte BeardIndex, + byte MoustacheIndex, + byte FaceAttachmentIndex, + Color SkinColor, + Color HairColor, + Color FacialHairColor, + ImmutableArray JobVariants) : INetSerializableStruct; + class CharacterInfoPrefab { public readonly ImmutableArray Heads; @@ -315,6 +341,8 @@ namespace Barotrauma public HashSet UnlockedTalents { get; private set; } = new HashSet(); + public (Identifier factionId, float reputation) MinReputationToHire; + /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection /// @@ -508,8 +536,11 @@ namespace Barotrauma public List CurrentOrders { get; } = new List(); - //unique ID given to character infos in MP - //used by clients to identify which infos are the same to prevent duplicate characters in round summary + + /// + /// Unique ID given to character infos in MP. Non-persistent. + /// Used by clients to identify which infos are the same to prevent duplicate characters in round summary. + /// public ushort ID; public List SpriteTags @@ -667,7 +698,6 @@ namespace Barotrauma { Name = GetRandomName(randSync); } - TryLoadNameAndTitle(npcIdentifier); SetPersonalityTrait(); @@ -824,6 +854,8 @@ namespace Barotrauma MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); UnlockedTalents = new HashSet(); + MinReputationToHire = (infoElement.GetAttributeIdentifier("factionId", Identifier.Empty), infoElement.GetAttributeFloat("minreputation", 0.0f)); + foreach (var subElement in infoElement.Elements()) { bool jobCreated = false; @@ -919,17 +951,25 @@ namespace Barotrauma } } + /// + /// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job. + /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. + /// public int GetIdentifier() { - return GetIdentifier(Name); + return GetIdentifierHash(Name); } + /// + /// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job. + /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. + /// public int GetIdentifierUsingOriginalName() { - return GetIdentifier(OriginalName); + return GetIdentifierHash(OriginalName); } - private int GetIdentifier(string name) + private int GetIdentifierHash(string name) { int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s))); id ^= Head.HairIndex << 12; @@ -1152,7 +1192,7 @@ namespace Barotrauma partial void LoadAttachmentSprites(); - private int CalculateSalary() + public int CalculateSalary() { if (Name == null || Job == null) { return 0; } @@ -1394,6 +1434,13 @@ namespace Barotrauma charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); + if (MinReputationToHire.factionId != default) + { + charElement.Add( + new XAttribute("factionId", Name), + new XAttribute("minreputation", MinReputationToHire.reputation)); + } + if (Character != null) { if (Character.AnimController.CurrentHull != null) @@ -1471,7 +1518,10 @@ namespace Barotrauma break; } } - targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); + targetAvailableInNextLevel = + !isOutside && + GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } && + (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); if (!targetAvailableInNextLevel) { if (!order.Prefab.CanBeGeneralized) @@ -1502,7 +1552,7 @@ namespace Barotrauma } if (order.OrderGiver != null) { - orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID)); + orderElement.Add(new XAttribute("ordergiver", order.OrderGiver.Info?.GetIdentifier())); } if (order.TargetSpatialEntity?.Submarine is Submarine targetSub) { @@ -1596,8 +1646,8 @@ namespace Barotrauma continue; } var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0); - int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1); - var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null; + int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1); + var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null; Entity targetEntity = null; switch (targetType) { @@ -1661,6 +1711,7 @@ namespace Barotrauma { targetId = GetOffsetId(parentSub, targetId); targetEntity = Entity.FindEntityByID(targetId); + return targetEntity != null; } else { @@ -1674,8 +1725,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead."); } + return true; } - return true; } } return orders; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index cb1b33763..d38730699 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; -using static Barotrauma.CharacterInfo; namespace Barotrauma { @@ -19,6 +17,8 @@ namespace Barotrauma public string Name => Identifier.Value; public Identifier VariantOf { get; } + public CharacterPrefab ParentPrefab { get; set; } + public void InheritFrom(CharacterPrefab parent) { ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); @@ -38,7 +38,7 @@ namespace Barotrauma } } - private XElement originalElement; + private readonly XElement originalElement; public ContentXElement ConfigElement { get; private set; } public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } @@ -49,10 +49,6 @@ namespace Barotrauma public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); - /// - /// Searches for a character config file from all currently selected content packages, - /// or from a specific package if the contentPackage parameter is given. - /// public static CharacterPrefab FindBySpeciesName(Identifier speciesName) { if (!Prefabs.ContainsKey(speciesName)) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 33368e40a..e6c09cc3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -14,11 +15,15 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } - public float PendingAdditionStrength { get; set; } - public float AdditionStrength { get; set; } + public float PendingGrainEffectStrength { get; set; } + public float GrainEffectStrength { get; set; } private float fluctuationTimer; + private AfflictionPrefab.Effect activeEffect; + private float prevActiveEffectStrength; + protected bool activeEffectDirty = true; + protected float _strength; [Serialize(0f, IsPropertySaveable.Yes), Editable] @@ -42,10 +47,11 @@ namespace Barotrauma float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); if (newValue > _strength) { - PendingAdditionStrength = Prefab.GrainBurst; + PendingGrainEffectStrength = Prefab.GrainBurst; Duration = Prefab.Duration; } _strength = newValue; + activeEffectDirty = true; } } @@ -68,8 +74,7 @@ namespace Barotrauma public float DamagePerSecondTimer; public float PreviousVitalityDecrease; - public float StrengthDiminishMultiplier = 1.0f; - public Affliction MultiplierSource; + public (float Value, Affliction Source) StrengthDiminishMultiplier = (1.0f, null); public readonly Dictionary PeriodicEffectTimers = new Dictionary(); @@ -95,7 +100,7 @@ namespace Barotrauma prefab?.ReloadSoundsIfNeeded(); #endif Prefab = prefab; - PendingAdditionStrength = Prefab.GrainBurst; + PendingGrainEffectStrength = Prefab.GrainBurst; _strength = strength; Identifier = prefab.Identifier; @@ -147,7 +152,16 @@ namespace Barotrauma MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)]; } - public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); + public AfflictionPrefab.Effect GetActiveEffect() + { + if (activeEffectDirty) + { + activeEffect = Prefab.GetActiveEffect(_strength); + prevActiveEffectStrength = _strength; + activeEffectDirty = false; + } + return activeEffect; + } public float GetVitalityDecrease(CharacterHealth characterHealth) { @@ -158,14 +172,14 @@ namespace Barotrauma { if (strength < Prefab.ActivationThreshold) { return 0.0f; } strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength); - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } float currVitalityDecrease = MathHelper.Lerp( currentEffect.MinVitalityDecrease, currentEffect.MaxVitalityDecrease, - (strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); if (currentEffect.MultiplyByMaxVitality) { @@ -186,11 +200,11 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinGrainStrength, currentEffect.MaxGrainStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); - if (Prefab.GrainBurst > 0 && AdditionStrength > amount) + if (Prefab.GrainBurst > 0 && GrainEffectStrength > amount) { - return Math.Min(AdditionStrength, 1.0f); + return Math.Min(GrainEffectStrength, 1.0f); } return amount; @@ -206,7 +220,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinScreenDistort, currentEffect.MaxScreenDistort, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetRadialDistortStrength() @@ -219,7 +233,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinRadialDistort, currentEffect.MaxRadialDistort, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetChromaticAberrationStrength() @@ -232,7 +246,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinChromaticAberration, currentEffect.MaxChromaticAberration, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetAfflictionOverlayMultiplier() @@ -247,7 +261,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinAfflictionOverlayAlphaMultiplier, currentEffect.MaxAfflictionOverlayAlphaMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public Color GetFaceTint() @@ -259,7 +273,7 @@ namespace Barotrauma return Color.Lerp( currentEffect.MinFaceTint, currentEffect.MaxFaceTint, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public Color GetBodyTint() @@ -271,7 +285,7 @@ namespace Barotrauma return Color.Lerp( currentEffect.MinBodyTint, currentEffect.MaxBodyTint, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetScreenBlurStrength() @@ -284,7 +298,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinScreenBlur, currentEffect.MaxScreenBlur, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect) @@ -302,7 +316,7 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinSkillMultiplier, currentEffect.MaxSkillMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); return amount; } @@ -333,7 +347,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinResistance, currentEffect.MaxResistance, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetSpeedMultiplier() @@ -344,26 +358,21 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinSpeedMultiplier, currentEffect.MaxSpeedMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetStatValue(StatTypes statType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; } - if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) - { - return MathHelper.Lerp( - value.minValue, - value.maxValue, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); - } - return 0.0f; + if (!currentEffect.AfflictionStatValues.TryGetValue(statType, out var appliedStat)) { return 0.0f; } + + return MathHelper.Lerp(appliedStat.MinValue, appliedStat.MaxValue, currentEffect.GetStrengthFactor(this)); } public bool HasFlag(AbilityFlags flagType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; } return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } @@ -401,13 +410,16 @@ namespace Barotrauma fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency; fluctuationTimer %= 1.0f; - if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted + if (currentEffect.StrengthChange < 0) // Only apply StrengthDiminish.Multiplier if affliction is being weakened { - float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier) - : characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier))); + float stat = characterHealth.Character.GetStatValue( + Prefab.IsBuff + ? StatTypes.BuffDurationMultiplier + : StatTypes.DebuffDurationMultiplier); - _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier; + float durationMultiplier = 1f / (1f + stat); + _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier.Value * durationMultiplier; } else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant { @@ -415,6 +427,7 @@ namespace Barotrauma } // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); + activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { @@ -426,14 +439,14 @@ namespace Barotrauma { amount /= Prefab.GrainBurst; } - if (PendingAdditionStrength >= 0) + if (PendingGrainEffectStrength >= 0) { - AdditionStrength += amount; - PendingAdditionStrength -= deltaTime; + GrainEffectStrength += amount; + PendingGrainEffectStrength -= deltaTime; } - else if (AdditionStrength > 0) + else if (GrainEffectStrength > 0) { - AdditionStrength -= amount; + GrainEffectStrength -= amount; } } @@ -442,7 +455,10 @@ namespace Barotrauma var currentEffect = GetActiveEffect(); if (currentEffect != null) { - currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb)); + foreach (var statusEffect in currentEffect.StatusEffects) + { + ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb); + } } } @@ -481,6 +497,7 @@ namespace Barotrauma { _nonClampedStrength = strength; _strength = _nonClampedStrength; + activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength); } public bool ShouldShowIcon(Character afflictedCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index 44643c736..bc8ac1329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// A special affliction type that increases the character's Bloodloss affliction with a rate relative to the strength of the bleeding. + /// class AfflictionBleeding : Affliction { public AfflictionBleeding(AfflictionPrefab prefab, float strength) : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 523835530..20a19d64f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -7,6 +7,10 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + /// + /// A special affliction type that gradually makes the character turn into another type of character. + /// See for more details. + /// partial class AfflictionHusk : Affliction { public enum InfectionState @@ -22,7 +26,7 @@ namespace Barotrauma private Character character; - private bool stun = true; + private bool stun = false; private readonly List huskInfection = new List(); @@ -43,6 +47,7 @@ namespace Barotrauma DeactivateHusk(); highestStrength = 0; } + activeEffectDirty = true; } } private float highestStrength; @@ -62,6 +67,7 @@ namespace Barotrauma private float DormantThreshold => HuskPrefab.DormantThreshold; private float ActiveThreshold => HuskPrefab.ActiveThreshold; private float TransitionThreshold => HuskPrefab.TransitionThreshold; + private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath; public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) @@ -216,7 +222,8 @@ namespace Barotrauma private void DeactivateHusk() { if (character?.AnimController == null || character.Removed) { return; } - if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) + if (Prefab is AfflictionPrefabHusk { NeedsAir: false } && + !character.CharacterHealth.GetAllAfflictions().Any(a => a != this && a.Prefab is AfflictionPrefabHusk { NeedsAir: false })) { character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 997d13a25..87b22795b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; using System.Collections.Immutable; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -56,8 +57,77 @@ namespace Barotrauma public override void Dispose() { } } + /// + /// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection. + /// class AfflictionPrefabHusk : AfflictionPrefab { + // Use any of these to define which limb the appendage is attached to. + // If multiple are defined, the order of preference is: id, name, type. + public readonly int AttachLimbId; + public readonly string AttachLimbName; + public readonly LimbType AttachLimbType; + + /// + /// The minimum strength at which husk infection will be in the dormant stage. + /// It must be less than or equal to ActiveThreshold. + /// + public readonly float DormantThreshold; + + /// + /// The minimum strength at which husk infection will be in the active stage. + /// It must be greater than or equal to DormantThreshold and less than or equal to TransitionThreshold. + /// + public readonly float ActiveThreshold; + + /// + /// The minimum strength at which husk infection will be in its final stage. + /// It must be greater than or equal to ActiveThreshold. + /// + public readonly float TransitionThreshold; + + /// + /// The minimum strength the affliction must have for the affected character + /// to transform into a husk upon death. + /// + public readonly float TransformThresholdOnDeath; + + /// + /// The species of husk to convert the affected character to + /// once husk infection reaches its final stage. + /// + public readonly Identifier HuskedSpeciesName; + + /// + /// If set to true, all buffs are transferred to the converted + /// character after husk transformation is complete. + /// + public readonly bool TransferBuffs; + + /// + /// If set to true, the affected player will see on-screen messages describing husk infection symptoms + /// and affected bots will speak about their current husk infection stage. + /// + public readonly bool SendMessages; + + /// + /// If set to true, affected characters will have their speech impeded once the affliction + /// reaches the dormant stage. + /// + public readonly bool CauseSpeechImpediment; + + /// + /// If not set to true, affected characters will no longer require air + /// once the affliction reaches the active stage. + /// + public readonly bool NeedsAir; + + /// + /// If set to true, affected players will retain control of their character + /// after transforming into a husk. + /// + public readonly bool ControlHusk; + public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type) { HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty); @@ -68,7 +138,6 @@ namespace Barotrauma } // Remove "[speciesname]" for backward support (we don't use it anymore) HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier(); - TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); if (TargetSpecies.Length == 0) { DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); @@ -79,7 +148,7 @@ namespace Barotrauma { AttachLimbId = attachElement.GetAttributeInt("id", -1); AttachLimbName = attachElement.GetAttributeString("name", null); - AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None; + AttachLimbType = attachElement.GetAttributeEnum("type", LimbType.None); } else { @@ -97,175 +166,282 @@ namespace Barotrauma DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f); ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f); TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength); + + if (DormantThreshold > ActiveThreshold) + { + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})"); + } + if (ActiveThreshold > TransitionThreshold) + { + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})"); + } + TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold); } - - // Use any of these to define which limb the appendage is attached to. - // If multiple are defined, the order of preference is: id, name, type. - public readonly int AttachLimbId; - public readonly string AttachLimbName; - public readonly LimbType AttachLimbType; - - public float ActiveThreshold, DormantThreshold, TransitionThreshold; - public float TransformThresholdOnDeath; - - public readonly Identifier HuskedSpeciesName; - public readonly Identifier[] TargetSpecies; - - public readonly bool TransferBuffs; - public readonly bool SendMessages; - public readonly bool CauseSpeechImpediment; - public readonly bool NeedsAir; - public readonly bool ControlHusk; } + /// + /// AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character. + /// There are multiple sub-types of afflictions such as AfflictionPrefabHusk, AfflictionPsychosis and AfflictionBleeding that can be used for additional functionality. + /// + /// When defining a new affliction, the type will be determined by the element name. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// class AfflictionPrefab : PrefabWithUintIdentifier { - public class Effect + /// + /// Effects are the primary way to add functionality to afflictions. + /// + /// + /// + /// + /// Enables the specified flag on the character as long as the effect is active. + /// + /// + /// + /// Flag that will be enabled for the character as long as the effect is active. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Which ability flag to enable. + /// + /// + /// + public sealed class Effect { //this effect is applied when the strength is within this range - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Minimum affliction strength required for this effect to be active.")] public float MinStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Maximum affliction strength for which this effect will be active.")] public float MaxStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's lowest strength.")] public float MinVitalityDecrease { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's highest strength.")] public float MaxVitalityDecrease { get; private set; } - //how much the strength of the affliction changes per second - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the affliction's strength changes every second while this effect is active.")] public float StrengthChange { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " + + "vilatily, with 1 meaning 100%, instead of the same amount for all species.")] public bool MultiplyByMaxVitality { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")] public float MinScreenBlur { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's highest strength.")] public float MaxScreenBlur { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's lowest strength.")] public float MinScreenDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's highest strength.")] public float MaxScreenDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's lowest strength.")] public float MinRadialDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's highest strength.")] public float MaxRadialDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's lowest strength.")] public float MinChromaticAberration { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's highest strength.")] public float MaxChromaticAberration { get; private set; } - [Serialize("255,255,255,255", IsPropertySaveable.No)] + [Serialize("255,255,255,255", IsPropertySaveable.No, description: "Radiation grain effect color.")] public Color GrainColor { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's lowest strength.")] public float MinGrainStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's highest strength.")] public float MaxGrainStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: + "The maximum rate of fluctuation to apply to visual effects caused by this affliction effect. " + + "Effective fluctuation is proportional to the affliction's current strength.")] public float ScreenEffectFluctuationFrequency { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for the affliction overlay's opacity at this effect's lowest strength. " + + "See the list of elements for more details.")] public float MinAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for the affliction overlay's opacity at this effect's highest strength. " + + "See the list of elements for more details.")] public float MaxAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for every buff's decay rate at this effect's lowest strength. " + + "Only applies to afflictions of class BuffDurationIncrease.")] public float MinBuffMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for every buff's decay rate at this effect's highest strength. " + + "Only applies to afflictions of class BuffDurationIncrease.")] public float MaxBuffMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's lowest strength.")] public float MinSpeedMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's highest strength.")] public float MaxSpeedMultiplier { get; private set; } - - [Serialize(1.0f, IsPropertySaveable.No)] + + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's lowest strength.")] public float MinSkillMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's highest strength.")] public float MaxSkillMultiplier { get; private set; } - private readonly Identifier[] resistanceFor; - public IReadOnlyList ResistanceFor => resistanceFor; + /// + /// A list of identifiers of afflictions that the affected character will be + /// resistant to when this effect is active. + /// + public readonly ImmutableArray ResistanceFor; - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, + description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")] public float MinResistance { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, + description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's highest strength.")] public float MaxResistance { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "Identifier used by AI to determine conversation lines to say when this effect is active.")] public Identifier DialogFlag { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "Tag that enemy AI may use to target the affected character when this effect is active.")] public Identifier Tag { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's face with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character's face.")] public Color MinFaceTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's face with at this effect's highest strength. The alpha channel is used to determine how much to tint the character's face.")] public Color MaxFaceTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's entire body with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character.")] public Color MinBodyTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")] public Color MaxBodyTint { get; private set; } /// - /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character + /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. /// - public Identifier[] BlockTransformation { get; private set; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public readonly struct AppliedStatValue + { + /// + /// Which StatType to apply + /// + public readonly StatTypes StatType; - public readonly Dictionary AfflictionStatValues = new Dictionary(); - public AbilityFlags AfflictionAbilityFlags; + /// + /// Minimum value to apply + /// + public readonly float MinValue; + + /// + /// Minimum value to apply + /// + public readonly float MaxValue; + + /// + /// Constant value to apply, will be ignored if MinValue or MaxValue are set + /// + private readonly float Value; + + public AppliedStatValue(ContentXElement element) + { + Value = element.GetAttributeFloat("value", 0.0f); + StatType = element.GetAttributeEnum("stattype", StatTypes.None); + MinValue = element.GetAttributeFloat("minvalue", Value); + MaxValue = element.GetAttributeFloat("maxvalue", Value); + } + } + + /// + /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character. + /// + public readonly ImmutableArray BlockTransformation; + + /// + /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. + /// + public readonly ImmutableDictionary AfflictionStatValues; + + public readonly AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active - public readonly List StatusEffects = new List(); + public readonly ImmutableArray StatusEffects; public Effect(ContentXElement element, string parentDebugName) { SerializableProperty.DeserializeProperties(this, element); - resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty()); - BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty()); + ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty())!.ToImmutableArray(); + BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty())!.ToImmutableArray(); + var afflictionStatValues = new Dictionary(); + var statusEffects = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": - StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); break; case "statvalue": - var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName); - - float defaultValue = subElement.GetAttributeFloat("value", 0f); - float minValue = subElement.GetAttributeFloat("minvalue", defaultValue); - float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue); - - AfflictionStatValues.TryAdd(statType, (minValue, maxValue)); + var newStatValue = new AppliedStatValue(subElement); + afflictionStatValues.Add(newStatValue.StatType, newStatValue); break; case "abilityflag": - var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); + AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None); + if (flagType is AbilityFlags.None) + { + DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\"."); + continue; + } AfflictionAbilityFlags |= flagType; break; case "affliction": @@ -273,21 +449,71 @@ namespace Barotrauma break; } } + AfflictionStatValues = afflictionStatValues.ToImmutableDictionary(); + StatusEffects = statusEffects.ToImmutableArray(); } + + /// + /// Returns 0 if affliction.Strength is MinStrength, + /// 1 if affliction.Strength is MaxStrength + /// + public float GetStrengthFactor(Affliction affliction) + => MathUtils.InverseLerp( + MinStrength, + MaxStrength, + affliction.Strength); } - public class Description + /// + /// Description element can be used to define descriptions for the affliction that are shown at specific conditions. + /// For example a description that only shows to other players or only at certain strength levels. + /// + /// + /// + /// Raw text for the description. + /// + /// + public sealed class Description { public enum TargetType { + /// + /// Everyone can see the description. + /// Any, + /// + /// Only the affected character can see the description. + /// Self, + /// + /// The affected character cannot see the description but others can. + /// OtherCharacter } + /// + /// Raw text for the description. + /// public readonly LocalizedString Text; + + /// + /// Text tag used to set the text from the localization files. + /// public readonly Identifier TextTag; - public readonly float MinStrength, MaxStrength; + + /// + /// Minimum strength required for the description to be shown. + /// + public readonly float MinStrength; + + /// + /// Maximum strength required for the description to be shown. + /// + public readonly float MaxStrength; + + /// + /// Who can see the description. + /// public readonly TargetType Target; public Description(ContentXElement element, AfflictionPrefab affliction) @@ -317,7 +543,23 @@ namespace Barotrauma } } - public class PeriodicEffect + /// + /// PeriodicEffect applies StatusEffects to the character periodically. + /// + /// + /// + /// + /// How often the status effect is applied in seconds. + /// Setting this attribute will set both the min and max interval to the specified value. + /// + /// + /// Minimum interval between applying the status effect in seconds. + /// + /// + /// Maximum interval between applying the status effect in seconds. + /// + /// + public sealed class PeriodicEffect { public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; @@ -344,65 +586,151 @@ namespace Barotrauma } } + public static readonly Identifier DamageType = "damage".ToIdentifier(); + public static readonly Identifier BurnType = "burn".ToIdentifier(); + public static readonly Identifier BleedingType = "bleeding".ToIdentifier(); + public static readonly Identifier ParalysisType = "paralysis".ToIdentifier(); + public static readonly Identifier PoisonType = "poison".ToIdentifier(); + public static readonly Identifier StunType = "stun".ToIdentifier(); + public static readonly Identifier EMPType = "emp".ToIdentifier(); + public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); + public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); + public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); + public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier(); + public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; - public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; - public static AfflictionPrefab Burn => Prefabs["burn"]; + public static AfflictionPrefab Bleeding => Prefabs[BleedingType]; + public static AfflictionPrefab Burn => Prefabs[BurnType]; public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; public static AfflictionPrefab Pressure => Prefabs["pressure"]; - public static AfflictionPrefab Stun => Prefabs["stun"]; + public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public override void Dispose() { } - public static IEnumerable List => Prefabs; - // Arbitrary string that is used to identify the type of the affliction. - public readonly Identifier AfflictionType; + public override void Dispose() { } private readonly ContentXElement configElement; - - //Does the affliction affect a specific limb or the whole character - public readonly bool LimbSpecific; - - //If not a limb-specific affliction, which limb is the indicator shown on in the health menu - //(e.g. mental health problems on head, lack of oxygen on torso...) - public readonly LimbType IndicatorLimb; public readonly LocalizedString Name; - public readonly Identifier TranslationIdentifier; - public readonly bool IsBuff; - public readonly bool AffectMachines; - public readonly bool HealableInMedicalClinic; - public readonly float HealCostMultiplier; - public readonly int BaseHealCost; - public readonly bool ShowBarInHealthMenu; - + public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; private readonly LocalizedString defaultDescription; public readonly ImmutableList Descriptions; + /// + /// Arbitrary string that is used to identify the type of the affliction. + /// + public readonly Identifier AfflictionType; + + /// + /// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character. + /// + public readonly bool LimbSpecific; + + /// + /// If the affliction doesn't affect individual limbs, this attribute determines + /// where the game will render the affliction's indicator when viewed in the + /// in-game health UI. + /// + /// For example, the psychosis indicator is rendered on the head, and low oxygen + /// is rendered on the torso. + /// + public readonly LimbType IndicatorLimb; + + /// + /// Can be set to the identifier of another affliction to make this affliction + /// reuse the same name and description. + /// + public readonly Identifier TranslationIdentifier; + + /// + /// If set to true, the game will recognize this affliction as a buff. + /// This means, among other things, that bots won't attempt to treat it, + /// and the health UI will render the affected limb in green rather than red. + /// + public readonly bool IsBuff; + + /// + /// If set to true, this affliction can affect characters that are marked as + /// machines, such as the Fractal Guardian. + /// + public readonly bool AffectMachines; + + /// + /// If set to true, this affliction can be healed at the medical clinic. + /// + /// + /// + /// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise. + /// + /// + public readonly bool HealableInMedicalClinic; + + /// + /// How much each unit of this affliction's strength will add + /// to the cost of healing at the medical clinic. + /// + public readonly float HealCostMultiplier; + + /// + /// The minimum cost of healing this affliction at the medical clinic. + /// + public readonly int BaseHealCost; + + /// + /// If set to false, the health UI will not show the strength of the affliction + /// as a bar under its indicator. + /// + public readonly bool ShowBarInHealthMenu; + + /// + /// If set to true, this affliction's icon will be hidden from the HUD + /// after 5 seconds. + /// public readonly bool HideIconAfterDelay; - //how high the strength has to be for the affliction to take affect + /// + /// How high the strength has to be for the affliction to take effect + /// public readonly float ActivationThreshold = 0.0f; - //how high the strength has to be for the affliction icon to be shown in the UI + + /// + /// How high the strength has to be for the affliction icon to be shown in the UI + /// public readonly float ShowIconThreshold = 0.05f; - //how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + + /// + /// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + /// public readonly float ShowIconToOthersThreshold = 0.05f; + + /// + /// The maximum strength this affliction can have. + /// public readonly float MaxStrength = 100.0f; + /// + /// The strength of the radiation grain effect to apply + /// when the strength of this affliction increases. + /// public readonly float GrainBurst; - //how high the strength has to be for the affliction icon to be shown with a health scanner + /// + /// How high the strength has to be for the affliction icon to be shown with a health scanner + /// public readonly float ShowInHealthScannerThreshold = 0.05f; - //how strong the affliction needs to be before bots attempt to treat it + /// + /// How strong the affliction needs to be before bots attempt to treat it + /// public readonly float TreatmentThreshold = 5.0f; /// @@ -411,25 +739,57 @@ namespace Barotrauma public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; /// - /// The affliction is automatically removed after this time. 0 = unlimited + /// The duration of the affliction, in seconds. If set to 0, the affliction does not expire. /// public readonly float Duration; - //how much karma changes when a player applies this affliction to someone (per strength of the affliction) + /// + /// How much karma changes when a player applies this affliction to someone (per strength of the affliction) + /// public float KarmaChangeOnApplied; + /// + /// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength. + /// public readonly float BurnOverlayAlpha; + + /// + /// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength. + /// public readonly float DamageOverlayAlpha; - //steam achievement given when the affliction is removed from the controlled character + /// + /// Steam achievement given when the controlled character receives the affliction + /// + public readonly Identifier AchievementOnReceived; + + /// + /// Steam achievement given when the affliction is removed from the controlled character + /// public readonly Identifier AchievementOnRemoved; - public readonly Sprite Icon; + /// + /// A gradient that defines which color to render this affliction's icon + /// with, based on the affliction's current strength. + /// public readonly Color[] IconColors; - public readonly Sprite AfflictionOverlay; + /// + /// If set to true and the affliction has an AfflictionOverlay element, + /// the overlay's opacity will be strictly proportional to its strength. + /// Otherwise, the overlay's opacity will be determined based on its + /// activation threshold and effects. + /// public readonly bool AfflictionOverlayAlphaIsLinear; + /// + /// If set to true, this affliction will not persist between rounds. + /// + public readonly bool ResetBetweenRounds; + + /// + /// Should damage particles be emitted when a character receives this affliction? Only relevant if the affliction is of the type "bleeding" or "damage". + /// public readonly bool DamageParticles; /// @@ -444,7 +804,20 @@ namespace Barotrauma /// public readonly float WeaponsSkillGain; + /// + /// A list of species this affliction is allowed to affect. + /// + public Identifier[] TargetSpecies { get; protected set; } + + /// + /// Effects to apply at various strength levels. + /// Only one effect can be applied at any given moment, so their ranges should be defined with no overlap. + /// private readonly List effects = new List(); + + /// + /// PeriodicEffect applies StatusEffects to the character periodically. + /// private readonly List periodicEffects = new List(); public IEnumerable Effects => effects; @@ -453,7 +826,17 @@ namespace Barotrauma private readonly ConstructorInfo constructor; - public readonly bool ResetBetweenRounds; + /// + /// Icon that's used in UI to represent this affliction. + /// + public readonly Sprite Icon; + + /// + /// A sprite that covers the affected player's entire screen when this affliction is active. + /// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier + /// and MaxAfflictionOverlayAlphaMultiplier + /// + public readonly Sprite AfflictionOverlay; public IEnumerable> TreatmentSuitability { @@ -481,7 +864,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(fallbackName)) { Name = Name.Fallback(fallbackName); - } + } defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); string fallbackDescription = element.GetAttributeString("description", ""); if (!string.IsNullOrEmpty(fallbackDescription)) @@ -536,13 +919,22 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f); - CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); - SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); + CauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", ""))) + .Fallback(element.GetAttributeString("causeofdeathdescription", "")); + SelfCauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", ""))) + .Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); IconColors = element.GetAttributeColorArray(nameof(IconColors), null); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); + AchievementOnReceived = element.GetAttributeIdentifier(nameof(AchievementOnReceived), ""); AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), ""); + TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); + ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs index 724184a11..a653041ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// A special affliction type that makes the character see and hear things that aren't there. + /// partial class AfflictionPsychosis : Affliction { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs index 408545fa2..fe656968e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs @@ -1,11 +1,12 @@ using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; namespace Barotrauma { + /// + /// A special affliction type that periodically inverts the character's controls and stuns the character. + /// The frequency and duration of the effects increases the higher the strength of the affliction is. + /// class AfflictionSpaceHerpes : Affliction { private float invertControlsCooldown = 60.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs index 730a08b4d..e3aaf11d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs @@ -3,6 +3,10 @@ using System; namespace Barotrauma { + /// + /// A special affliction type that increases the duration of buffs (afflictions of the type "buff"). The increase is defined using the + /// and attributes of the affliction effect. + /// class BuffDurationIncrease : Affliction { public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength) @@ -20,9 +24,9 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; } - affliction.MultiplierSource = null; - affliction.StrengthDiminishMultiplier = 1f; + if (!affliction.Prefab.IsBuff || affliction == this || affliction.StrengthDiminishMultiplier.Source != this) { continue; } + affliction.StrengthDiminishMultiplier.Source = null; + affliction.StrengthDiminishMultiplier.Value = 1f; } } else @@ -31,10 +35,10 @@ namespace Barotrauma { if (!affliction.Prefab.IsBuff || affliction == this) { continue; } float multiplier = GetDiminishMultiplier(); - if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; } + if (affliction.StrengthDiminishMultiplier.Value < multiplier && affliction.StrengthDiminishMultiplier.Source != this) { continue; } - affliction.MultiplierSource = this; - affliction.StrengthDiminishMultiplier = multiplier; + affliction.StrengthDiminishMultiplier.Source = this; + affliction.StrengthDiminishMultiplier.Value = multiplier; } } } @@ -48,7 +52,7 @@ namespace Barotrauma float multiplier = MathHelper.Lerp( currentEffect.MinBuffMultiplier, currentEffect.MaxBuffMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); return 1.0f / Math.Max(multiplier, 0.001f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index dcfea21dc..250337640 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -184,7 +184,7 @@ namespace Barotrauma } } - public Color DefaultFaceTint = Color.TransparentBlack; + public static readonly Color DefaultFaceTint = Color.TransparentBlack; public Color FaceTint { @@ -339,12 +339,12 @@ namespace Barotrauma return null; } - public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction + public T GetAffliction(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction { return GetAffliction(identifier, allowLimbAfflictions) as T; } - public Affliction GetAffliction(string identifier, Limb limb) + public Affliction GetAffliction(Identifier identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { @@ -380,7 +380,7 @@ namespace Barotrauma /// The limb the affliction is attached to /// Does the affliction have to be attached to only the specific limb. /// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb. - public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific) + public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific) { if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } @@ -401,7 +401,7 @@ namespace Barotrauma return strength; } - public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true) + public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true) { float strength = 0.0f; foreach (KeyValuePair kvp in afflictions) @@ -449,7 +449,11 @@ namespace Barotrauma var affliction = kvp.Key; resistance += affliction.GetResistance(afflictionPrefab.Identifier); } - return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + + resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + if (resistance > 1f) { resistance = 1f; } + + return resistance; } public float GetStatValue(StatTypes statType) @@ -483,16 +487,19 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } - + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(afflictions.Keys); - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + foreach (var affliction in afflictions) + { + if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) + { + matchingAfflictions.Add(affliction.Key); + } + } + ReduceMatchingAfflictions(amount, treatmentAction); } @@ -509,18 +516,21 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } - + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + var targetLimbHealth = limbHealths[targetLimb.HealthIndex]; + foreach (var affliction in afflictions) + { + if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) && + affliction.Value == targetLimbHealth) + { + matchingAfflictions.Add(affliction.Key); + } + } ReduceMatchingAfflictions(amount, treatmentAction); } @@ -622,7 +632,7 @@ namespace Barotrauma KillIfOutOfVitality(); } - public float GetLimbDamage(Limb limb, string afflictionType = null) + public float GetLimbDamage(Limb limb, Identifier afflictionType) { float damageStrength; if (limb.IsSevered) @@ -635,16 +645,16 @@ namespace Barotrauma // Therefore with e.g. 80 health, the max damage per limb would be 40. // Having at least 40 damage on both legs would cause maximum limping. float max = MaxVitality / 2; - if (string.IsNullOrEmpty(afflictionType)) + if (afflictionType.IsEmpty) { - float damage = GetAfflictionStrength("damage", limb, true); - float bleeding = GetAfflictionStrength("bleeding", limb, true); - float burn = GetAfflictionStrength("burn", limb, true); + float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true); + float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true); + float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true); damageStrength = Math.Min(damage + bleeding + burn, max); } else { - damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max); + damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max); } return damageStrength / max; } @@ -701,22 +711,17 @@ namespace Barotrauma if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) { - if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) - { - return; - } - } - if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; } - if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } - if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) - { - if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) + if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } } + if (Character.Params.Health.PoisonImmunity && + (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; } + if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; } + if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } Affliction existingAffliction = null; foreach (KeyValuePair kvp in afflictions) @@ -753,7 +758,9 @@ namespace Barotrauma Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); - + SteamAchievementManager.OnAfflictionReceived(copyAffliction, Character); + MedicalClinic.OnAfflictionCountChanged(Character); + Character.HealthUpdateInterval = 0.0f; CalculateVitality(); @@ -826,10 +833,16 @@ namespace Barotrauma } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } + foreach (var affliction in afflictionsToRemove) { afflictions.Remove(affliction); - } + } + + if (afflictionsToRemove.Count is not 0) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); @@ -883,6 +896,11 @@ namespace Barotrauma } } + /// + /// 0-1. + /// + public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab); + private void UpdateOxygen(float deltaTime) { if (!Character.NeedsOxygen) @@ -1137,16 +1155,14 @@ namespace Barotrauma } } - public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); - private readonly HashSet afflictionTags = new HashSet(); - public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) + public IEnumerable GetActiveAfflictionTags() { afflictionTags.Clear(); - foreach (Affliction affliction in afflictions) + foreach (Affliction affliction in afflictions.Keys) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !currentEffect.Tag.IsEmpty) + if (currentEffect is { Tag.IsEmpty: false }) { afflictionTags.Add(currentEffect.Tag); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e3d7bedf4..e7d13cf51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -27,6 +27,32 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float AimAccuracy { get; protected set; } + [Serialize(1f, IsPropertySaveable.No)] + public float SkillMultiplier { get; protected set; } + + [Serialize(0, IsPropertySaveable.No)] + public int ExperiencePoints { get; private set; } + + private readonly HashSet tags = new HashSet(); + + [Serialize("", IsPropertySaveable.Yes)] + public string Tags + { + get => string.Join(",", tags); + set + { + tags.Clear(); + if (!string.IsNullOrWhiteSpace(value)) + { + string[] splitTags = value.Split(','); + foreach (var tag in splitTags) + { + tags.Add(tag.ToIdentifier()); + } + } + } + } + private readonly HashSet moduleFlags = new HashSet(); [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")] @@ -79,6 +105,15 @@ namespace Barotrauma public Identifier[] PreferredOutpostModuleTypes { get; protected set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier Faction { get; set; } + + [Serialize("", IsPropertySaveable.No)] + public Identifier Group { get; set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowDraggingIndefinitely { get; set; } + public XElement Element { get; protected set; } @@ -97,6 +132,11 @@ namespace Barotrauma this.NpcSetIdentifier = npcSetIdentifier; } + public IEnumerable GetTags() + { + return tags; + } + public IEnumerable GetModuleFlags() { return moduleFlags; @@ -148,7 +188,7 @@ namespace Barotrauma } } - public bool GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) + public bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { if (ItemSets == null || !ItemSets.Any()) { return false; } var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; @@ -159,7 +199,7 @@ namespace Barotrauma int amount = itemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + InitializeItem(character, itemElement, submarine, this, spawnPoint, createNetworkEvents: createNetworkEvents); } } } @@ -177,17 +217,27 @@ namespace Barotrauma CharacterInfo characterInfo; if (characterElement == null) { - characterInfo= new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier); + characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync); } else { characterInfo = new CharacterInfo(characterElement, Identifier); } + if (characterInfo.Job != null && !MathUtils.NearlyEqual(SkillMultiplier, 1.0f)) + { + foreach (var skill in characterInfo.Job.GetSkills()) + { + float newSkill = skill.Level * SkillMultiplier; + skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false); + } + characterInfo.Salary = characterInfo.CalculateSalary(); + } characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier); + characterInfo.GiveExperience(ExperiencePoints); return characterInfo; } - public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true) + public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -231,7 +281,7 @@ namespace Barotrauma IdCard idCardComponent = item.GetComponent(); if (idCardComponent != null) { - idCardComponent.Initialize(null, character); + idCardComponent.Initialize(spawnPoint, character); if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) { idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; @@ -254,7 +304,7 @@ namespace Barotrauma int amount = childItemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); + InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 2db2aaadf..e311800bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -50,7 +50,7 @@ namespace Barotrauma public override void Dispose() { } } - class JobVariant + internal class JobVariant { public JobPrefab Prefab; public int Variant; @@ -113,7 +113,7 @@ namespace Barotrauma public readonly LocalizedString Name; - [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No, description: "How should the character behave when idling (not doing any particular task)?")] public AIObjectiveIdle.BehaviorType IdleBehavior { get; @@ -122,78 +122,63 @@ namespace Barotrauma public readonly LocalizedString Description; - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Can the character speak any random lines, or just ones specifically meant for the job?")] public bool OnlyJobSpecificDialog { get; private set; } - //the number of these characters in the crew the player starts with in the single player campaign - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "The number of these characters in the crew the player starts with in the single player campaign.")] public int InitialCount { get; private set; } - //if set to true, a client that has chosen this as their preferred job will get it no matter what - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, a client that has chosen this as their preferred job will get it regardless of the maximum number or the amount of spawnpoints in the sub.")] public bool AllowAlways { get; private set; } - //how many crew members can have the job (only one captain etc) - [Serialize(100, IsPropertySaveable.No)] + [Serialize(100, IsPropertySaveable.No, description: "How many crew members can have the job (e.g. only one captain etc).")] public int MaxNumber { get; private set; } - //how many crew members are REQUIRED to have the job - //(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference) - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "How many crew members are required to have the job. I.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference.")] public int MinNumber { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Minimum amount of karma a player must have to get assigned this job.")] public float MinKarma { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier on the base hiring cost when hiring the character from an outpost.")] public float PriceMultiplier { get; private set; } - // TODO: not used - [Serialize(10.0f, IsPropertySaveable.No)] - public float Commonness - { - get; - private set; - } - - //how much the vitality of the character is increased/reduced from the default value - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the vitality of the character is increased/reduced from the default value (e.g. 10 = 110 total vitality if the default vitality is 100.).")] public float VitalityModifier { get; private set; } - //whether the job should be available to NPCs - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Hidden jobs are not selectable by players, but can be used by e.g. outpost NPCs.")] public bool HiddenJob { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index 41a894960..e8e08f791 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -21,7 +21,7 @@ namespace Barotrauma level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } - private Identifier iconJobId; + private readonly Identifier iconJobId; public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab) ? jobPrefab.Icon diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 79d2eadc6..5cea1b66f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -666,13 +666,13 @@ namespace Barotrauma switch (body.BodyShape) { case PhysicsBody.Shape.Circle: - attack.DamageRange = body.radius; + attack.DamageRange = body.Radius; break; case PhysicsBody.Shape.Capsule: - attack.DamageRange = body.height / 2 + body.radius; + attack.DamageRange = body.Height / 2 + body.Radius; break; case PhysicsBody.Shape.Rectangle: - attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length(); + attack.DamageRange = new Vector2(body.Width / 2.0f, body.Height / 2.0f).Length(); break; } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); @@ -786,11 +786,12 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; - if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp") + if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { finalDamageModifier *= character.EmpVulnerability; } - if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")) + if (!character.Params.Health.PoisonImmunity && + (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { finalDamageModifier *= character.PoisonVulnerability; } @@ -1108,7 +1109,7 @@ namespace Barotrauma Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing); forceWorld.X *= character.AnimController.Dir; character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - if (!attack.IsRunning) + if (!attack.IsRunning && !attack.Ranged) { // Set the main collider where the body lands after the attack if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f) @@ -1225,7 +1226,7 @@ namespace Barotrauma if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.OnlyWhenDamagedByPlayer) { if (character.LastAttacker == null || !character.LastAttacker.IsPlayer) { @@ -1303,7 +1304,8 @@ namespace Barotrauma } private float blinkTimer; - private float blinkPhase; + public float BlinkPhase; + public bool FreezeBlinkState; private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; @@ -1316,16 +1318,25 @@ namespace Barotrauma { if (blinkTimer > -TotalBlinkDurationOut) { - blinkPhase -= deltaTime; - if (blinkPhase > 0) + if (!FreezeBlinkState) + { + BlinkPhase -= deltaTime; + } + if (BlinkPhase > 0) { // in - float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn)); + float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, BlinkPhase / Params.BlinkDurationIn)); body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + if (Params.UseTextureOffsetForBlinking) + { +#if CLIENT + ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetOut, Params.BlinkTextureOffsetIn, t); +#endif + } } else { - if (Math.Abs(blinkPhase) < Params.BlinkHoldTime) + if (Math.Abs(BlinkPhase) < Params.BlinkHoldTime) { // hold body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true); @@ -1333,15 +1344,25 @@ namespace Barotrauma else { // out - float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + //float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, (-BlinkPhase - Params.BlinkHoldTime) / Params.BlinkDurationOut)); body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + if (Params.UseTextureOffsetForBlinking) + { +#if CLIENT + ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetIn, Params.BlinkTextureOffsetOut, t); +#endif + } } } } else { // out - blinkPhase = Params.BlinkDurationIn; + if (!FreezeBlinkState) + { + BlinkPhase = Params.BlinkDurationIn; + } body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index e844e53a0..e902ba353 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -177,6 +177,9 @@ namespace Barotrauma set => SetFootAngles(FootAnglesInRadians, value); } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the animation be updated even if the character is not moving?"), Editable] + public bool UpdateAnimationWhenNotMoving { get; set; } + /// /// Key = limb id, value = angle in radians /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 04204dc96..ba88edee5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -56,6 +56,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool ShowHealthBar { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseBossHealthBar { get; private set; } @@ -110,6 +113,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool DrawLast { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable] + public float AITurretPriority { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable] + public float AISlowTurretPriority { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -214,7 +223,7 @@ namespace Barotrauma return true; } - public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group; + public static bool CompareGroup(Identifier group1, Identifier group2) => group1 != Identifier.Empty && group2 != Identifier.Empty && group1 == group2; protected void CreateSubParams() { @@ -476,7 +485,7 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes), Editable] public bool DoesBleed { get; set; } - [Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + [Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)] public float CrushDepth { get; set; } // Make editable? @@ -512,7 +521,20 @@ namespace Barotrauma // TODO: limbhealths, sprite? - public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { } + public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) + { + //backwards compatibility + if (CrushDepth < 0) + { + //invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level) + float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000; + DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+ + "Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+ + "but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+ + $"Changing the crush depth from {CrushDepth} to {newCrushDepth}."); + CrushDepth = newCrushDepth; + } + } } public class InventoryParams : SubParam @@ -615,6 +637,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool UsePathFindingToGetInside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] public bool KeepDoorsClosed { get; private set; } @@ -823,10 +848,19 @@ namespace Barotrauma [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + [Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")] + public bool IgnoreTargetSize { get; private set; } + + [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] public float CircleRotationSpeed { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + [Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable] + public bool DynamicCircleRotationSpeed { get; private set; } + + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] + public float CircleRandomRotationFactor { get; private set; } + + [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index d8f8992b0..be86bd432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -6,6 +6,7 @@ using System.Linq; using Barotrauma.IO; using System.Xml; using Barotrauma.Extensions; +using FarseerPhysics; #if CLIENT using Barotrauma.SpriteDeformations; #endif @@ -621,13 +622,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Radius { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Height { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Width { get; set; } [Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] @@ -706,6 +707,15 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyBlinkInWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool UseTextureOffsetForBlinking { get; set; } + + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)] + public Vector2 BlinkTextureOffsetIn { get; set; } + + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)] + public Vector2 BlinkTextureOffsetOut { get; set; } + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionIn { get; private set; } @@ -1026,15 +1036,18 @@ namespace Barotrauma } } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Radius { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Height { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Width { get; set; } + [Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable] + public BodyType BodyType { get; set; } + public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) { Name = name; 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 c07128f0f..26af153b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -19,15 +19,9 @@ namespace Barotrauma.Abilities foreach (XElement subElement in conditionElement.Elements()) { - if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) + if (subElement.NameAsIdentifier() == "conditional") { - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 23512f751..a71667245 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -14,45 +14,51 @@ namespace Barotrauma.Abilities { string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; HashSet missionTypes = new HashSet(); + isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); + foreach (string missionTypeString in missionTypeStrings) { if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) { - DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); - return; + if (!isAffiliated) + { + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + } + continue; } missionTypes.Add(parsedMission); } missionType = missionTypes.ToImmutableHashSet(); - isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityMission { Mission: { } mission }) { - if (isAffiliated) + if (!isAffiliated) { return CheckMissionType(); } + + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + foreach (var (factionIdentifier, amount) in mission.ReputationRewards) { - if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } - - foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + if (amount <= 0) { continue; } + if (GetMatchingFaction(factionIdentifier) is { } faction && + Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { - if (amount <= 0) { continue; } - - Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier); - - if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) - { - return true; - } + return CheckMissionType(); } - - return false; } - return missionType.Contains(mission.Prefab.Type); + return false; + + Faction GetMatchingFaction(Identifier factionIdentifier) => + factionIdentifier == "location" + ? mission.OriginLocation?.Faction + : factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier); + + bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type); } LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); 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 c4017a87f..237e15b5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -23,7 +23,6 @@ protected override bool MatchesConditionSpecific() { Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); - return character.Info.GetSavedStatValue(statType, identifier) >= min; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 55c382e2a..7a4ceeb07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -4,6 +4,7 @@ using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma.Abilities { @@ -29,11 +30,33 @@ namespace Barotrauma.Abilities if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } + ImmutableHashSet characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + HashSet> talentsTrees = new HashSet>(); foreach (TalentSubTree subTree in talentTree.TalentSubTrees) { if (subTree.Type != TalentTreeType.Specialization) { continue; } - talentsTrees.Add(subTree.AllTalentIdentifiers); + + HashSet identifiers = new HashSet(); + foreach (TalentOption option in subTree.TalentOptionStages) + { + foreach (Identifier identifier in option.TalentIdentifiers) + { + if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; } + + identifiers.Add(identifier); + } + + foreach (var (_, value) in option.ShowCaseTalents) + { + var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet(); + if (ids.Count is 0) { continue; } + + identifiers.Add(value.GetRandomUnsynced()); + } + } + + talentsTrees.Add(identifiers.ToImmutableHashSet()); } ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); @@ -44,6 +67,16 @@ namespace Barotrauma.Abilities Character.GiveTalent(identifier); } + + static bool IsShowCaseTalent(Identifier identifier, TalentOption option) + { + foreach (var (_, value) in option.ShowCaseTalents) + { + if (value.Contains(identifier)) { return true; } + } + + return false; + } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index dc93405b4..d14c9df8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -131,6 +131,8 @@ namespace Barotrauma if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } + if (IsTalentLocked(talentIdentifier)) { return false; } + foreach (var subTree in talentTree!.TalentSubTrees) { if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; } @@ -152,6 +154,18 @@ namespace Barotrauma return false; } + public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet characterList = null) + { + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + foreach (Character c in characterList) + { + if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; } + } + + return false; + } + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) { List viableTalents = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs new file mode 100644 index 000000000..4c1ff0527 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs @@ -0,0 +1,15 @@ +namespace Barotrauma +{ + sealed class SlideshowsFile : GenericPrefabFile + { + protected override PrefabCollection Prefabs => SlideshowPrefab.Prefabs; + + public SlideshowsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "Slideshow"; + + protected override bool MatchesPlural(Identifier identifier) => identifier == "Slideshows"; + + protected override SlideshowPrefab CreatePrefab(ContentXElement element) => new SlideshowPrefab(this, element); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 88ff41f52..8ea7cb235 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -116,9 +116,12 @@ namespace Barotrauma public static void ThrowIfDuplicates(IEnumerable pkgs) { var contentPackages = pkgs as IList ?? pkgs.ToArray(); - if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2))) + foreach (ContentPackage cp in contentPackages) { - throw new InvalidOperationException($"Input contains duplicate packages"); + if (contentPackages.AtLeast(2, cp2 => cp == cp2)) + { + throw new InvalidOperationException($"Input contains duplicate packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})"); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 428050ab2..74937c8ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -1,12 +1,11 @@ #nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection.Metadata.Ecma335; using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -64,9 +63,14 @@ namespace Barotrauma public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); - public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); - [return:NotNullIfNotNull("def")] - public ImmutableHashSet? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); + + [return: NotNullIfNotNull("def")] + public Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys) => Element.GetAttributeIdentifierArray(def, keys); + [return: NotNullIfNotNull("def")] + public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + [return: NotNullIfNotNull("def")] + public ImmutableHashSet GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); + public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index a6cb260a5..0cb3e03cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -384,7 +384,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - PermissionPreset.List.Select(pp => pp.Name.Value).ToArray() + PermissionPreset.List.Select(pp => pp.DisplayName.Value).ToArray() }; })); @@ -737,7 +737,7 @@ namespace Barotrauma commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) => { Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); - if (revivedCharacter == null) return; + if (revivedCharacter == null) { return; } revivedCharacter.Revive(); #if SERVER @@ -745,7 +745,7 @@ namespace Barotrauma { foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.Character != revivedCharacter) continue; + if (c.Character != revivedCharacter) { continue; } //clients stop controlling the character when it dies, force control back GameMain.Server.SetClientCharacter(c, revivedCharacter); @@ -889,7 +889,15 @@ namespace Barotrauma ThrowError("Please specify an identifier and a value."); return; } - SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + if (float.TryParse(args[1], out float floatVal)) + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), floatVal, SetDataAction.OperationType.Set); + } + else + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + } + }, isCheat: true)); commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) => @@ -1091,11 +1099,6 @@ namespace Barotrauma commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null) { return; } - if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null) - { - NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red); - return; - } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { @@ -1260,6 +1263,22 @@ namespace Barotrauma } #endif + commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) => + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + NewMessage("Reputation:"); + foreach (var faction in campaign.Factions) + { + NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}"); + } + } + else + { + ThrowError("Could not show reputation (no active campaign)."); + } + }, null)); + commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) => { if (GameMain.GameSession?.GameMode is CampaignMode campaign) @@ -1267,7 +1286,7 @@ namespace Barotrauma if (args.Length == 0) { return; } if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation); + campaign.Map.CurrentLocation.Reputation?.SetReputation(reputation); } else { @@ -1424,7 +1443,7 @@ namespace Barotrauma commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => { Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); }, () => { @@ -1860,6 +1879,7 @@ namespace Barotrauma commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false)); + commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true)); commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); @@ -1869,6 +1889,8 @@ namespace Barotrauma commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true)); commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true)); commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true)); + commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true)); InitProjectSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index b9695d7dd..e20724d51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -12,21 +12,112 @@ namespace Barotrauma Exponential } + /// + /// ActionTypes define when a is executed. + /// public enum ActionType { - Always = 0, OnPicked = 1, OnUse = 2, OnSecondaryUse = 3, - OnWearing = 4, OnContaining = 5, OnContained = 6, OnNotContained = 7, - OnActive = 8, OnFailure = 9, OnBroken = 10, - OnFire = 11, InWater = 12, NotInWater = 13, + /// + /// Executes every frame regardless of the state of the entity. + /// + Always = 0, + /// + /// Executes when the item is picked up. Only valid for items. + /// + OnPicked = 1, + /// + /// Executes when the item is used. The meaning of "using" an item depends on the item, but generally it means the action that happens when holding the item and clicking LMB. Only valid for items. + /// + OnUse = 2, + /// + /// Executes when an item is held and the aim key is held. Only valid for items. + /// + OnSecondaryUse = 3, + /// + /// Executes continuously while the item is being worn. Only valid for wearable items. + /// + OnWearing = 4, + /// + /// Executes continuously when a specific Containable is inside an ItemContainer. Only valid for Containables defined in an ItemContainer component. + /// + OnContaining = 5, + /// + /// Executes continuously when the item is contained in some inventory. Only valid for items. + /// + OnContained = 6, + /// + /// Executes continuously when the item is NOT contained in an inventory. Only valid for items. + /// + OnNotContained = 7, + /// + /// Executes continuously when the item is active. The meaning of "active" depends on the item, but generally means the item is on, powered, and doing the thing it's intended for. Only valid for items. + /// + OnActive = 8, + /// + /// Executes when using the item fails due to a failed skill check. Only valid for items. + /// + OnFailure = 9, + /// + /// Executes when using the item's condition drops to 0. Only valid for items. + /// + OnBroken = 10, + /// + /// Executes continuously when the entity is within the damage range of fire. Valid for items and characters. + /// + OnFire = 11, + /// + /// Executes continuously when the entity is submerged. Valid for items and characters. + /// + InWater = 12, + /// + /// Executes continuously when the entity is NOT submerged. Valid for items and characters. + /// + NotInWater = 13, + /// + /// Executes when the entity hits something hard enough. For items, the threshold is determined by , + /// for characters by . Valid for items and characters. + /// OnImpact = 14, + /// + /// Executes continuously when the character is eating another character. Only valid for characters. + /// OnEating = 15, + /// + /// Executes when the entity receives damage from an external source (i.e. an affliction that increases in severity, or an item degrading by itself don't count). + /// Valid for items and characters. + /// OnDamaged = 16, + /// + /// Executes when the limb gets severed. Only valid for limbs. + /// OnSevered = 17, + /// + /// Executes when a produces an item (e.g. when a plant grows a fruit). Only valid for Growable items. + /// OnProduceSpawned = 18, - OnOpen = 19, OnClose = 20, + /// + /// Executes when a is opened. Only valid for doors. + /// + OnOpen = 19, + /// + /// Executes when a is closed. Only valid for doors. + /// + OnClose = 20, + /// + /// Executes when the entity spawns. Only valid for doors. + /// OnSpawn = 21, + /// + /// Executes when using the item succeeds based on a skill check. Only valid for items. + /// OnSuccess = 22, + /// + /// Executes when an Ability (an effect from a talent) triggers the status effect. Only valid in Abilities, the target can be either a character or an item depending on the type of Ability. + /// OnAbility = 23, + /// + /// Executes when the character dies. Only valid for characters. + /// OnDeath = OnBroken } @@ -75,86 +166,391 @@ namespace Barotrauma OnStatusEffectIdentifier, } + /// + /// StatTypes are used to alter several traits of a character. They are mostly used by talents. + /// + /// A lot of StatTypes use a "percentage" value. The way this works is that the value is 0 by default and 1 is added to the value of the stat type to get the final multiplier. + /// For example if the value is set to 0.2 then 1 is added to it making it 1.2 and that is used as a multiplier. + /// This makes it so values between -100% and +100% can be easily represented as -1 and 1 respectively. For example 0.5 would translate to 1.5 for +50% and -0.2 would translate to 0.8 for -20% multiplier. + /// public enum StatTypes { + /// + /// Used to indicate an invalid stat type. Should not be used. + /// None, - // Skills + + /// + /// Boosts electrical skill by a flat amount. + /// ElectricalSkillBonus, + + /// + /// Boosts helm skill by a flat amount. + /// HelmSkillBonus, - HelmSkillOverride, - MedicalSkillOverride, - WeaponsSkillOverride, - ElectricalSkillOverride, - MechanicalSkillOverride, + + /// + /// Boosts mechanical skill by a flat amount. + /// MechanicalSkillBonus, + + /// + /// Boosts medical skill by a flat amount. + /// MedicalSkillBonus, + + /// + /// Boosts weapons skill by a flat amount. + /// WeaponsSkillBonus, - // Character attributes + + /// + /// Boosts the character's helm skill to the given value if it's lower than the given value. + /// + HelmSkillOverride, + + /// + /// Boosts the character's medical skill to the given value if it's lower than the given value. + /// + MedicalSkillOverride, + + /// + /// Boosts the character's weapons skill to the given value if it's lower than the given value. + /// + WeaponsSkillOverride, + + /// + /// Boosts the character's electrical skill to the given value if it's lower than the given value. + /// + ElectricalSkillOverride, + + /// + /// Boosts the character's mechanical skill to the given value if it's lower than the given value. + /// + MechanicalSkillOverride, + + /// + /// Increases character's maximum vitality by a percentage. + /// MaximumHealthMultiplier, + + /// + /// Increases both walking and swimming speed of the character by a percentage. + /// MovementSpeed, + + /// + /// Increases the character's walking speed by a percentage. + /// WalkingSpeed, + + /// + /// Increases the character's swimming speed by a percentage. + /// SwimmingSpeed, + + /// + /// Decreases how long it takes for buffs applied to the character decay over time by a percentage. + /// Buffs are afflictions that have isBuff set to true. + /// BuffDurationMultiplier, + + /// + /// Decreases how long it takes for debuff applied to the character decay over time by a percentage. + /// Debuffs are afflictions that have isBuff set to false. + /// DebuffDurationMultiplier, + + /// + /// Increases the strength of afflictions that are applied to the character by a percentage. + /// Medicines are items that have the "medical" tag. + /// MedicalItemEffectivenessMultiplier, + + /// + /// Increases the resistance to pushing force caused by flowing water by a percentage. The resistance cannot be below 0% or higher than 100%. + /// FlowResistance, - // Combat + + /// + /// Increases how much damage the character deals via all attacks by a percentage. + /// AttackMultiplier, + + /// + /// Increases how much damage the character deals to other characters on the same team by a percentage. + /// TeamAttackMultiplier, + + /// + /// Decreases the reload time of ranged weapons held by the character by a percentage. + /// RangedAttackSpeed, + + /// + /// Decreases the reload time of submarine turrets operated by the character by a percentage. + /// TurretAttackSpeed, + + /// + /// Decreases the power consumption of submarine turrets operated by the character by a percentage. + /// TurretPowerCostReduction, + + /// + /// Increases how fast submarine turrets operated by the character charge up by a percentage. Affects turrets like pulse laser. + /// TurretChargeSpeed, + + /// + /// Increases how fast the character can swing melee weapons by a percentage. + /// MeleeAttackSpeed, + + /// + /// Increases the damage dealt by melee weapons held by the character by a percentage. + /// MeleeAttackMultiplier, - RangedAttackMultiplier, + + /// + /// Decreases the spread of ranged weapons held by the character by a percentage. + /// RangedSpreadReduction, - // Utility + + /// + /// Increases the repair speed of the character by a percentage. + /// RepairSpeed, + + /// + /// Increases the repair speed of the character when repairing mechanical items by a percentage. + /// MechanicalRepairSpeed, + + /// + /// Increase deconstruction speed of deconstructor operated by the character by a percentage. + /// DeconstructorSpeedMultiplier, + + /// + /// Increases the repair speed of repair tools that fix submarine walls by a percentage. + /// RepairToolStructureRepairMultiplier, + + /// + /// Increases the wall damage of tools that destroy submarine walls like plasma cutter by a percentage. + /// RepairToolStructureDamageMultiplier, + + /// + /// Increase the detach speed of items like minerals that require a tool to detach from the wall by a percentage. + /// RepairToolDeattachTimeMultiplier, + + /// + /// Allows the character to repair mechanical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair mechanical items to 110% condition. + /// MaxRepairConditionMultiplierMechanical, + + /// + /// Allows the character to repair electrical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair electrical items to 110% condition. + /// MaxRepairConditionMultiplierElectrical, + + /// + /// Increase the the quality of items crafted by the character by a flat amount. + /// Can be made to only affect certain item with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all items. + /// IncreaseFabricationQuality, + + /// + /// Boosts the condition of genes combined by the character by a flat amount. + /// GeneticMaterialRefineBonus, + + /// + /// Reduces the chance to taint a gene when combining genes by a percentage. Tainting probability can not go below 0% or above 100%. + /// GeneticMaterialTaintedProbabilityReductionOnCombine, + + /// + /// Increases the speed at which the character gains skills by a percentage. + /// SkillGainSpeed, + + /// + /// Whenever the character's skill level up add a flat amount of more skill levels to the character. + /// ExtraLevelGain, + + /// + /// Increases the speed at which the character gains helm skill by a percentage. + /// HelmSkillGainSpeed, + + /// + /// Increases the speed at which the character gains weapons skill by a percentage. + /// WeaponsSkillGainSpeed, + + /// + /// Increases the speed at which the character gains medical skill by a percentage. + /// MedicalSkillGainSpeed, + + /// + /// Increases the speed at which the character gains electrical skill by a percentage. + /// ElectricalSkillGainSpeed, + + /// + /// Increases the speed at which the character gains mechanical skill by a percentage. + /// MechanicalSkillGainSpeed, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Medicines are items that have the "medical" tag. + /// MedicalItemApplyingMultiplier, - MedicalItemDurationMultiplier, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Works only for afflictions that have isBuff set to true. + /// + BuffItemApplyingMultiplier, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Works only for afflictions that have "poison" type. + /// PoisonMultiplier, - // Tinker + + /// + /// Increases how long the character can tinker with items by a flat amount where 1 = 1 second. + /// TinkeringDuration, + + /// + /// Increases the effectiveness of the character's tinkerings by a percentage. + /// Tinkering strength affects the speed and effectiveness of the item that is being tinkered with. + /// TinkeringStrength, + + /// + /// Increases how much condition tinkered items lose when the character tinkers with them by a percentage. + /// TinkeringDamage, - // Misc + + /// + /// Increases how much reputation the character gains by a percentage. + /// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions. + /// ReputationGainMultiplier, + + /// + /// Increases how much reputation the character loses by a percentage. + /// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions. + /// ReputationLossMultiplier, + + /// + /// Increases how much money the character gains from missions by a percentage. + /// MissionMoneyGainMultiplier, + + /// + /// Increases how much talent experience the character gains from all sources by a percentage. + /// ExperienceGainMultiplier, + + /// + /// Increases how much talent experience the character gains from missions by a percentage. + /// MissionExperienceGainMultiplier, + + /// + /// Increases how many missions the characters crew can have at the same time by a flat amount. + /// ExtraMissionCount, + + /// + /// Increases how many items are in stock in special sales in the store by a flat amount. + /// ExtraSpecialSalesCount, + + /// + /// Increases how much money is gained from selling items to the store by a percentage. + /// StoreSellMultiplier, + + /// + /// Decreases the prices of items in affiliated store by a percentage. + /// StoreBuyMultiplierAffiliated, + + /// + /// Decreases the prices of items in all stores by a percentage. + /// StoreBuyMultiplier, + + /// + /// Decreases the price of upgrades and submarines in affiliated outposts by a percentage. + /// + ShipyardBuyMultiplierAffiliated, + + /// + /// Decreases the price of upgrades and submarines in all outposts by a percentage. + /// + ShipyardBuyMultiplier, + + /// + /// Limits how many of a certain item can be attached to the wall in the submarine at the same time. + /// Has to be used with CharacterAbilityGivePermanentStat to specify the tag of the item that is affected. Does nothing if no tag is specified. + /// MaxAttachableCount, + + /// + /// Increase the radius of explosions caused by the character by a percentage. + /// ExplosionRadiusMultiplier, + + /// + /// Increases the damage of explosions caused by the character by a percentage. + /// ExplosionDamageMultiplier, + + /// + /// Decreases the time it takes to fabricate items on fabricators operated by the character by a percentage. + /// FabricationSpeed, + + /// + /// Increases how much damage the character deals to ballast flora by a percentage. + /// BallastFloraDamageMultiplier, + + /// + /// Increases the time it takes for the character to pass out when out of oxygen. + /// HoldBreathMultiplier, + + /// + /// Used to set the character's apprencticeship to a certain job. + /// Used by the "apprenticeship" talent and requires a job to be specified via CharacterAbilityGivePermanentStat. + /// Apprenticeship, - Affiliation, - CPRBoost + + /// + /// Increases the revival chance of the character when performing CPR by a percentage. + /// + CPRBoost, + + /// + /// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat. + /// + LockedTalents } internal enum ItemTalentStats @@ -172,22 +568,77 @@ namespace Barotrauma FabricationSpeed } + /// + /// AbilityFlags are a set of toggleable flags that can be applied to characters. + /// [Flags] public enum AbilityFlags { + /// + /// Used to indicate an erroneous ability flag. Should not be used. + /// None = 0, + + /// + /// Character will not be able to run. + /// MustWalk = 0x1, + + /// + /// Character is immune to pressure. + /// ImmuneToPressure = 0x2, + + /// + /// Character won't be targeted by enemy AI. + /// IgnoredByEnemyAI = 0x4, + + /// + /// Character can drag corpses without a movement speed penalty. + /// MoveNormallyWhileDragging = 0x8, + + /// + /// Character is able to tinker with items. + /// CanTinker = 0x10, + + /// + /// Character is able to tinker with fabricators and deconstructors. + /// CanTinkerFabricatorsAndDeconstructors = 0x20, + + /// + /// Allows items tinkered by the character to consume no power. + /// TinkeringPowersDevices = 0x40, + + /// + /// Allows the character to gain skills past 100. + /// GainSkillPastMaximum = 0x80, + + /// + /// Allows the character to retain experience when respawning as a new character. + /// RetainExperienceForNewCharacter = 0x100, + + /// + /// Allows CharacterAbilityApplyStatusEffectsToLastOrderedCharacter to affect the last 2 characters ordered. + /// AllowSecondOrderedTarget = 0x200, + + /// + /// Character will stay conscious even if their vitality drops below 0. + /// AlwaysStayConscious = 0x400, - CanNotDieToAfflictions = 0x800, + + /// + /// Prevents afflictions on the character from dropping the characters vitality below the kill threshold. + /// The character can still die from sources like getting crushed by pressure or if their head is severed. + /// + CanNotDieToAfflictions = 0x800 } [Flags] @@ -225,4 +676,4 @@ namespace Barotrauma Local, Radio } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 2275bcd06..dcd1deeea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -110,7 +110,7 @@ namespace Barotrauma state = 1; break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return; + if (!Submarine.MainSub.AtEitherExit) { return; } Finish(); state = 2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 192664d17..1b41e3bf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -9,6 +9,8 @@ namespace Barotrauma public event Action Finished; protected bool isFinished; + public int RandomSeed; + protected readonly EventPrefab prefab; public EventPrefab Prefab => prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index cce7bd22c..af4f0c713 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -15,20 +16,13 @@ namespace Barotrauma { DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed."); } - foreach (var attribute in element.Attributes()) - { - if (PropertyConditional.IsValid(attribute) && !IsTargetTagAttribute(attribute)) - { - Conditional = new PropertyConditional(attribute); - break; - } - } + Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault(); if (Conditional == null) { DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); } - static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag"; + static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; } private string GetEventName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index 47c205aee..bd30a9b4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -21,7 +21,7 @@ namespace Barotrauma protected object? value2; protected object? value1; - protected PropertyConditional.OperatorType Operator { get; set; } + protected PropertyConditional.ComparisonOperatorType Operator { get; set; } public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { @@ -56,23 +56,13 @@ namespace Barotrauma { if (GameMain.GameSession?.GameMode is not CampaignMode campaignMode) { return false; } - string[] splitString = Condition.Split(' '); - string value; - if (splitString.Length > 0) + (Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition); + if (Operator == PropertyConditional.ComparisonOperatorType.None) { - //the first part of the string is the operator, skip it - value = string.Join(" ", splitString.Skip(1)); - } - else - { - DebugConsole.ThrowError($"{Condition} is too short, it should start with an operator followed by a boolean or a floating point value."); + DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value."); return false; } - string op = splitString[0]; - Operator = PropertyConditional.GetOperatorType(op); - if (Operator == PropertyConditional.OperatorType.None) { return false; } - if (CheckAgainstMetadata) { object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier); @@ -82,8 +72,8 @@ namespace Barotrauma { return Operator switch { - PropertyConditional.OperatorType.Equals => metadata1 == metadata2, - PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2, + PropertyConditional.ComparisonOperatorType.Equals => metadata1 == metadata2, + PropertyConditional.ComparisonOperatorType.NotEquals => metadata1 != metadata2, _ => false }; } @@ -139,9 +129,9 @@ namespace Barotrauma value2 = val2; switch (Operator) { - case PropertyConditional.OperatorType.Equals: + case PropertyConditional.ComparisonOperatorType.Equals: return val1 == val2; - case PropertyConditional.OperatorType.NotEquals: + case PropertyConditional.ComparisonOperatorType.NotEquals: return val1 != val2; default: DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2})."); @@ -166,17 +156,17 @@ namespace Barotrauma value2 = val2; switch (Operator) { - case PropertyConditional.OperatorType.Equals: + case PropertyConditional.ComparisonOperatorType.Equals: return MathUtils.NearlyEqual(val1, val2); - case PropertyConditional.OperatorType.GreaterThan: + case PropertyConditional.ComparisonOperatorType.GreaterThan: return val1 > val2; - case PropertyConditional.OperatorType.GreaterThanEquals: + case PropertyConditional.ComparisonOperatorType.GreaterThanEquals: return val1 >= val2; - case PropertyConditional.OperatorType.LessThan: + case PropertyConditional.ComparisonOperatorType.LessThan: return val1 < val2; - case PropertyConditional.OperatorType.LessThanEquals: + case PropertyConditional.ComparisonOperatorType.LessThanEquals: return val1 <= val2; - case PropertyConditional.OperatorType.NotEquals: + case PropertyConditional.ComparisonOperatorType.NotEquals: return !MathUtils.NearlyEqual(val1, val2); } @@ -195,9 +185,9 @@ namespace Barotrauma bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase); switch (Operator) { - case PropertyConditional.OperatorType.Equals: + case PropertyConditional.ComparisonOperatorType.Equals: return equals; - case PropertyConditional.OperatorType.NotEquals: + case PropertyConditional.ComparisonOperatorType.NotEquals: return !equals; default: DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2})."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index e84bd7676..d202db5b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -33,7 +33,7 @@ namespace Barotrauma public int ItemContainerIndex { get; set; } private readonly IReadOnlyList conditionals; - + private readonly Identifier[] itemIdentifierSplit; private readonly Identifier[] itemTags; @@ -44,13 +44,7 @@ namespace Barotrauma var conditionalList = new List(); foreach (ContentXElement subElement in element.GetChildElements("conditional")) { - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionalList.Add(new PropertyConditional(attribute)); - } - } + conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); break; } conditionals = conditionalList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 1c5c8f0b5..a01b1abd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -200,7 +200,7 @@ namespace Barotrauma } } - private int[] GetEndingOptions() + public int[] GetEndingOptions() { List endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList(); if (!ContinueConversation) { endings.Add(-1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 2c298853d..be2f85fbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -1,12 +1,13 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { - class MissionAction : EventAction + partial class MissionAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionIdentifier { get; set; } @@ -14,8 +15,10 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionTag { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] - public string LocationType { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier RequiredFaction { get; set; } + + public ImmutableArray LocationTypes { get; } [Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] public int MinLocationDistance { get; set; } @@ -28,6 +31,8 @@ namespace Barotrauma private bool isFinished; + private readonly Random random; + public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) @@ -38,6 +43,8 @@ namespace Barotrauma { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } + LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); + random = new MTRandom(parentEvent.RandomSeed); } public override bool IsFinished(ref string goTo) @@ -56,14 +63,14 @@ namespace Barotrauma if (GameMain.GameSession.GameMode is CampaignMode campaign) { Mission unlockedMission = null; - var unlockLocation = FindUnlockLocation(); + var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes); if (unlockLocation == null && CreateLocationIfNotFound) { //find an empty location at least 3 steps away, further on the map - var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet()); + var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable()); if (emptyLocation != null) { - emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]); + emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]); unlockLocation = emptyLocation; } } @@ -72,11 +79,11 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } else if (!MissionTag.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag); + unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random); } if (campaign is MultiPlayerCampaign mpCampaign) { @@ -84,7 +91,9 @@ namespace Barotrauma } if (unlockedMission != null) { - if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null) + unlockedMission.OriginLocation = campaign.Map.CurrentLocation; + 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}\"."); } @@ -99,66 +108,86 @@ namespace Barotrauma IconColor = unlockedMission.Prefab.IconColor }; #else + missionsUnlockedThisRound.Add(unlockedMission); NotifyMissionUnlock(unlockedMission); - #endif +#endif } } else { - DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); + DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); } } isFinished = true; } - private Location FindUnlockLocation() + private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable locationTypes) { var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1) + if (LocationTypes.Length == 0 && minDistance <= 1) { return campaign.Map.CurrentLocation; } - return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet()); + var currentLocation = campaign.Map.CurrentLocation; + int distance = 0; + HashSet checkedLocations = new HashSet(); + HashSet pendingLocations = new HashSet() { currentLocation }; + do + { + List currentLocations = pendingLocations.ToList(); + pendingLocations.Clear(); + foreach (var location in currentLocations) + { + checkedLocations.Add(location); + if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes)) + { + return location; + } + else + { + foreach (LocationConnection connection in location.Connections) + { + var otherLocation = connection.OtherLocation(location); + if (checkedLocations.Contains(otherLocation)) { continue; } + pendingLocations.Add(otherLocation); + } + } + } + distance++; + } while (pendingLocations.Any()); + + return null; } - private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet checkedLocations) + private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable locationTypes) { - var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance && - (!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X)) + if (!RequiredFaction.IsEmpty) { - return currLocation; + if (location.Faction?.Prefab.Identifier != RequiredFaction && + location.SecondaryFaction?.Prefab.Identifier != RequiredFaction) + { + return false; + } } - checkedLocations.Add(currLocation); - foreach (LocationConnection connection in currLocation.Connections) + if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier()))) { - var otherLocation = connection.OtherLocation(currLocation); - if (checkedLocations.Contains(otherLocation)) { continue; } - var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations); - if (unlockLocation != null) { return unlockLocation; } + return false; } - return null; + if (distance < minDistance) + { + return false; + } + if (unlockFurtherOnMap && location.MapPosition.X < currLocation.MapPosition.X) + { + return false; + } + return true; } public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } - -#if SERVER - private void NotifyMissionUnlock(Mission mission) - { - foreach (Client client in GameMain.Server.ConnectedClients) - { - IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); - outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); - outmsg.WriteIdentifier(mission.Prefab.Identifier); - outmsg.WriteString(mission.Name.Value); - GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); - } - } -#endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs new file mode 100644 index 000000000..d6deb9b1c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -0,0 +1,66 @@ +namespace Barotrauma +{ + class MissionStateAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } + + public enum OperationType + { + Set, + Add + } + + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + public OperationType Operation { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int State { get; set; } + + private bool isFinished; + + public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + State = element.GetAttributeInt("value", State); + if (MissionIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured."); + } + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + foreach (Mission mission in GameMain.GameSession.Missions) + { + if (mission.Prefab.Identifier != MissionIdentifier) { continue; } + switch (Operation) + { + case OperationType.Set: + mission.State = State; + break; + case OperationType.Add: + mission.State += 1; + break; + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs new file mode 100644 index 000000000..013b48771 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -0,0 +1,91 @@ +namespace Barotrauma +{ + class ModifyLocationAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Faction { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SecondaryFaction { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Name { get; set; } + + private bool isFinished; + + public ModifyLocationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + if (GameMain.GameSession.GameMode is CampaignMode campaign) + { + var location = campaign.Map.CurrentLocation; + if (location != null) + { + if (!Faction.IsEmpty) + { + var faction = campaign.Factions.Find(f => f.Prefab.Identifier == Faction); + if (faction == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\"."); + } + else + { + location.Faction = faction; + } + } + if (!SecondaryFaction.IsEmpty) + { + var secondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == SecondaryFaction); + if (secondaryFaction == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\"."); + } + else + { + location.SecondaryFaction = secondaryFaction; + } + } + if (!Type.IsEmpty) + { + var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == Type); + if (locationType == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\"."); + } + else if (!location.LocationTypeChangesBlocked) + { + location.ChangeType(campaign, locationType); + } + } + if (!string.IsNullOrEmpty(Name)) + { + location.ForceName(TextManager.Get(Name).Fallback(Name).Value); + } + } + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ModifyLocationAction)}"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index ee26b9283..7905486c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,8 +1,6 @@ -using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -12,7 +10,7 @@ namespace Barotrauma public Identifier NPCTag { get; set; } [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] - public CharacterTeamType TeamTag { get; set; } + public CharacterTeamType TeamID { get; set; } [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } @@ -24,10 +22,13 @@ namespace Barotrauma public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { + //backwards compatibility + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); - if (!enums.Contains(TeamTag)) + if (!enums.Contains(TeamID)) { - DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); + DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); } } @@ -41,27 +42,34 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { // characters will still remain on friendlyNPC team for rest of the tick - npc.SetOriginalTeam(TeamTag); - - if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2)) + npc.SetOriginalTeam(TeamID); + foreach (Item item in npc.Inventory.AllItems) + { + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.TeamID = TeamID; + } + } + if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2)) { npc.Info.StartItemsGiven = true; GameMain.GameSession.CrewManager.AddCharacter(npc); ChangeItemTeam(Submarine.MainSub, true); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems)); } } else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2)) { npc.Info.StartItemsGiven = true; GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true); - var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamTag); + var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID); ChangeItemTeam(sub, false); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamTag, npc.Inventory.AllItems)); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems)); } } @@ -72,11 +80,10 @@ namespace Barotrauma item.AllowStealing = allowStealing; if (item.GetComponent() is { } wifiComponent) { - wifiComponent.TeamID = TeamTag; + wifiComponent.TeamID = TeamID; } if (item.GetComponent() is { } idCard) { - idCard.TeamID = TeamTag; idCard.SubmarineSpecificID = 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index cee49e531..e0d23114b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -39,13 +39,14 @@ namespace Barotrauma affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { - if (!(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Follow) { var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true) { - OverridePriority = 100.0f + OverridePriority = 100.0f, + IsFollowOrderObjective = true }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index fa3b3d2f8..f05962579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -19,8 +19,6 @@ namespace Barotrauma private IEnumerable affectedNpcs; - private AIObjectiveGoTo gotoObjective; - public override void Update(float deltaTime) { if (isFinished) { return; } @@ -33,19 +31,18 @@ namespace Barotrauma if (Wait) { - gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) + var gotoObjective = new AIObjectiveGoTo( + AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true) { - OverridePriority = 100.0f + OverridePriority = 100.0f, + SourceEventAction = this }; humanAiController.ObjectiveManager.AddObjective(gotoObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; } else { - if (gotoObjective != null) - { - gotoObjective.Abandon = true; - } + AbandonGoToObjectives(humanAiController); } } isFinished = true; @@ -62,17 +59,25 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || npc.AIController is not HumanAIController) { continue; } - if (gotoObjective != null) - { - gotoObjective.Abandon = true; - } + if (npc.Removed || npc.AIController is not HumanAIController aiController) { continue; } + AbandonGoToObjectives(aiController); } affectedNpcs = null; } isFinished = false; } + private void AbandonGoToObjectives(HumanAIController aiController) + { + foreach (var objective in aiController.ObjectiveManager.Objectives) + { + if (objective is AIObjectiveGoTo gotoObjective && gotoObjective.SourceEventAction?.ParentEvent == ParentEvent) + { + gotoObjective.Abandon = true; + } + } + } + public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCWaitAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, Wait: {Wait.ColorizeObject()})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index d39a5e666..348e856e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma { @@ -11,17 +9,20 @@ namespace Barotrauma public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier ItemIdentifier { get; set; } + public string ItemIdentifiers { get; set; } [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { - if (ItemIdentifier.IsEmpty) + private readonly ImmutableHashSet itemIdentifierSplit; + + public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (string.IsNullOrEmpty(ItemIdentifiers)) { - ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty)); + ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty)); } + itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet(); } private bool isFinished = false; @@ -62,7 +63,7 @@ namespace Barotrauma var item = inventory.FindItem(it => it != null && !removedItems.Contains(it) && - (ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true); + (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(it.Prefab.Identifier)), recursive: true); if (item == null) { break; } Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); @@ -70,7 +71,7 @@ namespace Barotrauma } else if (target is Item item) { - if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier) + if (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(item.Prefab.Identifier)) { Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 14be7bdba..41c9391f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -46,43 +46,29 @@ namespace Barotrauma switch (TargetType) { case ReputationType.Faction: - { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); - if (faction != null) { - faction.Reputation.AddReputation(Increase); - } - else - { - DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); - } - - break; - } - case ReputationType.Location: - { - Location location = campaign.Map.CurrentLocation; - if (location != null) - { - location.Reputation.AddReputation(Increase); - IEnumerable locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location); - foreach (Location connectedLocation in locations) + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); + if (faction != null) { - Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null"); - if (connectedLocation.Reputation != null) - { - connectedLocation.Reputation.AddReputation(Increase / 4); - } + faction.Reputation.AddReputation(Increase); + } + else + { + DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); } - } - break; - } + break; + } + case ReputationType.Location: + { + campaign.Map.CurrentLocation?.Reputation?.AddReputation(Increase); + break; + } default: - { - DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); - break; - } + { + DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); + break; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index ea2851339..f015fc26b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -50,7 +50,7 @@ namespace Barotrauma public Identifier SpawnPointTag { get; set; } [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] - public CharacterTeamType Team { get; protected set; } + public CharacterTeamType TeamID { get; protected set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] public bool RequireSpawnPointTag { get; set; } @@ -92,6 +92,14 @@ namespace Barotrauma public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; + //backwards compatibility + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + if (element.GetAttribute("submarinetype") != null) + { + DebugConsole.ThrowError( + $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " + + $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?"); + } } public override bool IsFinished(ref string goTo) @@ -118,7 +126,28 @@ namespace Barotrauma if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty) { - HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); + HumanPrefab humanPrefab = null; + if (Level.Loaded?.StartLocation is Location startLocation) + { + humanPrefab = + TryFindHumanPrefab(startLocation.Faction) ?? + TryFindHumanPrefab(startLocation.SecondaryFaction); + } + HumanPrefab TryFindHumanPrefab(Faction faction) + { + if (faction == null) { return null; } + return + NPCSet.Get(NPCSetIdentifier, + NPCIdentifier.Replace("[faction]".ToIdentifier(), faction.Prefab.Identifier), + logError: false) ?? + //try to spawn a coalition NPC if a correct one can't be found + NPCSet.Get(NPCSetIdentifier, + NPCIdentifier.Replace("[faction]".ToIdentifier(), "coalition".ToIdentifier()), + logError: false); + } + + humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true); + if (humanPrefab != null) { if (!AllowDuplicates && @@ -130,13 +159,13 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { if (newCharacter == null) { return; } newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = Team; + newCharacter.TeamID = TeamID; newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); if (LootingIsStealing) { foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) @@ -151,6 +180,18 @@ namespace Barotrauma ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + } + } +#if SERVER + newCharacter.LoadTalents(); + GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); +#endif }); } } @@ -165,7 +206,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => { if (!TargetTag.IsEmpty && newCharacter != null) { @@ -211,7 +252,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); } } else @@ -239,10 +280,10 @@ namespace Barotrauma spawned = true; } - public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount) + public static Vector2 OffsetSpawnPos(Vector2 pos, float offset) { - Hull hull = Hull.FindHull(pos); - pos += Rand.Vector(offsetAmount); + Hull hull = Hull.FindHull(pos); + pos += Rand.Vector(offset); if (hull != null) { float margin = 50.0f; @@ -289,30 +330,24 @@ namespace Barotrauma public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; - List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); - - potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); - + List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); + potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable); if (moduleFlags != null && moduleFlags.Any()) { - List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList(); + var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains)); if (spawnPoints.Any()) { - potentialSpawnPoints = spawnPoints; + potentialSpawnPoints = spawnPoints.ToList(); } } - if (spawnpointTags != null && spawnpointTags.Any()) { - var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - + var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable)); if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.None()) { if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index 4dc8d6adc..f07c316cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma { @@ -7,7 +6,7 @@ namespace Barotrauma { private readonly List effects = new List(); - private int actionIndex; + private readonly int actionIndex; [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -46,25 +45,40 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } - var targets = ParentEvent.GetTargets(TargetTag); + var eventTargets = ParentEvent.GetTargets(TargetTag); foreach (StatusEffect effect in effects) { - foreach (var target in targets) + foreach (var target in eventTargets) { - if (target is Item targetItem) + if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); - } - else - { - effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + List nearbyTargets = new List(); + effect.AddNearbyTargets(target.WorldPosition, nearbyTargets); + foreach (var nearbyTarget in nearbyTargets) + { + ApplyOnTarget(nearbyTarget as Entity, effect); + } + continue; } + ApplyOnTarget(target, effect); } } #if SERVER - ServerWrite(targets); + ServerWrite(eventTargets); #endif isFinished = true; + + void ApplyOnTarget(Entity target, StatusEffect effect) + { + if (target is Item targetItem) + { + effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); + } + else + { + effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + } + } } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 89eff4f39..ef66959e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -21,6 +21,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes)] public bool IgnoreIncapacitatedCharacters { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowHiddenItems { get; set; } + private bool isFinished = false; public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) @@ -119,12 +122,12 @@ namespace Barotrauma private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); } private void TagHullsByName(Identifier name) @@ -137,6 +140,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, 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); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index c7253cc72..51e265f83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -1,12 +1,13 @@ -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { class TriggerEventAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier Identifier { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool NextRound { get; set; } + private bool isFinished; public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -26,17 +27,24 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { - var eventPrefab = EventSet.GetEventPrefab(Identifier); - if (eventPrefab == null) + if (NextRound) { - DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + GameMain.GameSession.EventManager.QueuedEventsForNextRound.Enqueue(Identifier); } else { - var ev = eventPrefab.CreateInstance(); - if (ev != null) + var eventPrefab = EventSet.GetEventPrefab(Identifier); + if (eventPrefab == null) { - GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + } + else + { + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index 6380f8f0d..202a8734d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -1,6 +1,3 @@ -using System; -using System.Xml.Linq; - namespace Barotrauma { class WaitAction : EventAction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index dee7f7b0f..47d8f4ddf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1,9 +1,11 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -12,6 +14,7 @@ namespace Barotrauma public enum NetworkEventType { CONVERSATION, + CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, UNLOCKPATH @@ -72,8 +75,7 @@ namespace Barotrauma private readonly List activeEvents = new List(); private readonly HashSet finishedEvents = new HashSet(); - private readonly HashSet nonRepeatableEvents = new HashSet(); - private readonly HashSet usedUniqueSets = new HashSet(); + private readonly HashSet nonRepeatableEvents = new HashSet(); #if DEBUG && SERVER @@ -100,7 +102,9 @@ namespace Barotrauma public readonly Queue QueuedEvents = new Queue(); - private struct TimeStamp + public readonly Queue QueuedEventsForNextRound = new Queue(); + + private readonly struct TimeStamp { public readonly double Time; public readonly Event Event; @@ -122,7 +126,8 @@ namespace Barotrauma public bool Enabled = true; - private MTRandom rand; + private MTRandom random; + private int randomSeed; public void StartRound(Level level) { @@ -134,7 +139,9 @@ namespace Barotrauma pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); - +#if SERVER + MissionAction.ResetMissionsUnlockedThisRound(); +#endif pathFinder = new PathFinder(WayPoint.WayPointList, false); totalPathLength = 0.0f; if (level != null) @@ -144,23 +151,22 @@ namespace Barotrauma } SelectSettings(); - - int seed = 0; + if (level != null) { - seed = ToolBox.StringToInt(level.Seed); + randomSeed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); + randomSeed ^= ToolBox.IdentifierToInt(previousEvent); } } - rand = new MTRandom(seed); + random = new MTRandom(randomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; EventSet initialEventSet = SelectRandomEvents( EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign, - random: rand); + random: random); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { @@ -168,7 +174,7 @@ namespace Barotrauma initialEventSet = SelectRandomEvents( EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: playingCampaign, - random: rand); + random: random); } if (initialEventSet != null) { @@ -188,14 +194,7 @@ namespace Barotrauma //if the outpost is connected to a locked connection, create an event to unlock it if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); - var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => - e.BiomeIdentifier.IsEmpty || - e.BiomeIdentifier == level.LevelData.Biome.Identifier); - - var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? - ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : - ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); + var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); @@ -216,7 +215,7 @@ namespace Barotrauma { foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { - nonRepeatableEvents.Add(ep); + nonRepeatableEvents.Add(ep.Identifier); } } foreach (EventSet childSet in eventSet.ChildSets) @@ -226,6 +225,21 @@ namespace Barotrauma } } + while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id) + { + var eventPrefab = EventSet.GetEventPrefab(id); + if (eventPrefab == null) + { + DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); + continue; + } + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + QueuedEvents.Enqueue(ev); + } + } + PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; @@ -358,7 +372,6 @@ namespace Barotrauma QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); - usedUniqueSets.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); @@ -370,20 +383,49 @@ namespace Barotrauma /// /// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history /// - public void RegisterEventHistory() + public void RegisterEventHistory(bool registerFinishedOnly = false) { if (level?.LevelData == null) { return; } - level.LevelData.EventsExhausted = true; + level.LevelData.EventsExhausted = !registerFinishedOnly; + if (level.LevelData.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); + if (registerFinishedOnly) + { + foreach (var finishedEvent in finishedEvents) + { + var key = finishedEvent.ParentSet; + if (key == null) { continue; } + if (level.LevelData.FinishedEvents.ContainsKey(key)) + { + level.LevelData.FinishedEvents[key] += 1; + } + else + { + level.LevelData.FinishedEvents.Add(key, 1); + } + } + } + + level.LevelData.EventHistory.AddRange(selectedEvents.Values + .SelectMany(v => v) + .Select(e => e.Prefab.Identifier) + .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId))); + if (level.LevelData.EventHistory.Count > MaxEventHistory) { level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); } } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); + + if (!registerFinishedOnly) + { + level.LevelData.FinishedEvents.Clear(); + } + + bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); } public void SkipEventCooldown() @@ -393,9 +435,9 @@ namespace Barotrauma private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { - if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; } + if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; } float retVal = baseCommonness; - if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; } + if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; } return retVal; } @@ -436,9 +478,13 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab e) - => (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && - !level.LevelData.NonRepeatableEvents.Contains(e); + bool isPrefabSuitable(EventPrefab e) => + (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + !level.LevelData.NonRepeatableEvents.Contains(e.Identifier) && + isFactionSuitable(e.Faction); + + bool isFactionSuitable(Identifier factionId) => + factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; foreach (var subEventPrefab in eventSet.EventPrefabs) { @@ -447,9 +493,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\"."); } } - + var suitablePrefabSubsets = eventSet.EventPrefabs.Where( - e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); + e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); for (int i = 0; i < applyCount; i++) { @@ -462,14 +508,14 @@ namespace Barotrauma for (int j = 0; j < eventCount; j++) { if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } - EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand); + EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random); (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; - if (eventPrefabs != null && rand.NextDouble() <= probability) + if (eventPrefabs != null && random.NextDouble() <= probability) { - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); - + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } + newEvent.RandomSeed = randomSeed; if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -483,7 +529,7 @@ namespace Barotrauma } if (eventSet.ChildSets.Any()) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); + var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); if (newEventSet != null) { CreateEvents(newEventSet); @@ -494,9 +540,9 @@ namespace Barotrauma { foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { - if (rand.NextDouble() > probability) { continue; } + if (random.NextDouble() > probability) { continue; } - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } if (!selectedEvents.ContainsKey(eventSet)) @@ -601,6 +647,10 @@ namespace Barotrauma private bool IsValidForLocation(EventSet eventSet, Location location) { if (location is null) { return true; } + if (!eventSet.Faction.IsEmpty) + { + if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; } + } var locationType = location.GetLocationType(); bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents; if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; } @@ -728,53 +778,50 @@ namespace Barotrauma calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; } - if (currentIntensity < eventThreshold) + bool recheck = false; + do { - bool recheck = false; - do + recheck = false; + //activate pending event sets that can be activated + for (int i = pendingEventSets.Count - 1; i >= 0; i--) { - recheck = false; - //activate pending event sets that can be activated - for (int i = pendingEventSets.Count - 1; i >= 0; i--) + var eventSet = pendingEventSets[i]; + if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } + if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; } + if (!CanStartEventSet(eventSet)) { continue; } + + pendingEventSets.RemoveAt(i); + + if (selectedEvents.ContainsKey(eventSet)) { - var eventSet = pendingEventSets[i]; - if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } - - if (!CanStartEventSet(eventSet)) { continue; } - - pendingEventSets.RemoveAt(i); - - if (selectedEvents.ContainsKey(eventSet)) + //start events in this set + foreach (Event ev in selectedEvents[eventSet]) { - //start events in this set - foreach (Event ev in selectedEvents[eventSet]) + activeEvents.Add(ev); + eventThreshold = settings.DefaultEventThreshold; + if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) { - activeEvents.Add(ev); - eventThreshold = settings.DefaultEventThreshold; - if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) + eventCoolDown = settings.EventCooldown; + } + if (eventSet.ResetTime > 0) + { + ev.Finished += () => { - eventCoolDown = settings.EventCooldown; - } - if (eventSet.ResetTime > 0) - { - ev.Finished += () => - { - pendingEventSets.Add(eventSet); - CreateEvents(eventSet); - }; - } + pendingEventSets.Add(eventSet); + CreateEvents(eventSet); + }; } } - - //add child event sets to pending - foreach (EventSet childEventSet in eventSet.ChildSets) - { - pendingEventSets.Add(childEventSet); - recheck = true; - } } - } while (recheck); - } + + //add child event sets to pending + foreach (EventSet childEventSet in eventSet.ChildSets) + { + pendingEventSets.Add(childEventSet); + recheck = true; + } + } + } while (recheck); foreach (Event ev in activeEvents) { @@ -782,11 +829,11 @@ namespace Barotrauma { ev.Update(deltaTime); } - else if (!finishedEvents.Contains(ev)) + else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab)) { if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) { - if (!level.LevelData.EventHistory.Contains(ev.Prefab)) { level.LevelData.EventHistory.Add(ev.Prefab); } + if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } } finishedEvents.Add(ev); } @@ -832,30 +879,43 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; } + if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; } - if (!(character.AIController is EnemyAIController enemyAI)) { continue; } + if (character.AIController is EnemyAIController enemyAI) + { + if (!enemyAI.AIParams.StayInAbyss) + { + // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. + monsterStrength += enemyAI.CombatStrength; + } - if (!enemyAI.AIParams.StayInAbyss) + if (character.CurrentHull?.Submarine?.Info != null && + (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && + character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) + { + // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 + enemyDanger += enemyAI.CombatStrength / 500.0f; + } + else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) + { + // Enemy outside targeting the sub or something in it + // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. + enemyDanger += enemyAI.CombatStrength / 5000.0f; + } + } + else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1)) { - // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. - monsterStrength += enemyAI.CombatStrength; - } - - if (character.CurrentHull?.Submarine?.Info != null && - (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && - character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) - { - // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 - enemyDanger += enemyAI.CombatStrength / 500.0f; - } - else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) - { - // Enemy outside targeting the sub or something in it - // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. - enemyDanger += enemyAI.CombatStrength / 5000.0f; + if (character.Submarine != null && + Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange) + { + //we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character), + //so let's just go with a fixed value. + //5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max + enemyDanger += 0.2f; + } } } + // Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub. // On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4. // And if they get inside, we add 0.1 per crawler on that. @@ -1108,5 +1168,20 @@ namespace Barotrauma return false; } + + public void Load(XElement element) + { + foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty())) + { + QueuedEventsForNextRound.Enqueue(id); + } + } + + public XElement Save() + { + return new XElement("eventmanager", + new XAttribute(nameof(QueuedEventsForNextRound), + string.Join(',', QueuedEventsForNextRound))); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index db5fe38b8..7bd5e7e01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,6 +1,7 @@ using System; +using System.Linq; using System.Reflection; -using System.Xml.Linq; +using System.Reflection.Emit; namespace Barotrauma { @@ -14,12 +15,12 @@ namespace Barotrauma public readonly bool TriggerEventCooldown; public readonly float Commonness; public readonly Identifier BiomeIdentifier; + public readonly Identifier Faction; public readonly float SpawnDistance; public readonly bool UnlockPathEvent; public readonly string UnlockPathTooltip; public readonly int UnlockPathReputation; - public readonly string UnlockPathFaction; public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) @@ -40,6 +41,7 @@ namespace Barotrauma } BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); + Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); @@ -47,7 +49,6 @@ namespace Barotrauma UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); - UnlockPathFaction = element.GetAttributeString("unlockpathfaction", ""); SpawnDistance = element.GetAttributeFloat("spawndistance", 0); } @@ -80,5 +81,17 @@ namespace Barotrauma { return $"EventPrefab ({Identifier})"; } + + public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction) + { + var unlockPathEvents = Prefabs.OrderBy(p => p.Identifier).Where(e => e.UnlockPathEvent); + if (faction != null && unlockPathEvents.Any(e => e.Faction == faction.Prefab.Identifier)) + { + unlockPathEvents = unlockPathEvents.Where(e => e.Faction == faction.Prefab.Identifier); + } + return + unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ?? + unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index a5f16c350..b5409cc22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -1,10 +1,9 @@ -using System; +using Barotrauma.Extensions; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -89,7 +88,9 @@ namespace Barotrauma public readonly LevelData.LevelType LevelType; public readonly ImmutableArray LocationTypeIdentifiers; - + + public readonly Identifier Faction; + public readonly bool ChooseRandom; private readonly int eventCount = 1; @@ -110,6 +111,8 @@ namespace Barotrauma public readonly bool IgnoreCoolDown; + public readonly bool IgnoreIntensity; + public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; @@ -143,11 +146,12 @@ namespace Barotrauma public readonly struct SubEventPrefab { - public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) + public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability, Identifier factionId) { PrefabOrIdentifier = prefabOrIdentifiers; SelfCommonness = commonness; SelfProbability = probability; + Faction = factionId; } public readonly Either PrefabOrIdentifier; @@ -178,6 +182,8 @@ namespace Barotrauma public readonly float? SelfProbability; public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f; + public readonly Identifier Faction; + public void Deconstruct(out IEnumerable eventPrefabs, out float commonness, out float probability) { eventPrefabs = EventPrefabs; @@ -260,6 +266,8 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); } + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null); if (locationTypeStr != null) { @@ -282,6 +290,7 @@ namespace Barotrauma PerWreck = element.GetAttributeBool("perwreck", false); DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); + IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); @@ -332,15 +341,17 @@ namespace Barotrauma Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); float commonness = subElement.GetAttributeFloat("commonness", -1f); float probability = subElement.GetAttributeFloat("probability", -1f); + Identifier factionId = subElement.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); eventPrefabs.Add(new SubEventPrefab( identifiers, commonness >= 0f ? commonness : (float?)null, - probability >= 0f ? probability : (float?)null)); + probability >= 0f ? probability : (float?)null, + factionId)); } else { var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier()); - eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); + eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability, prefab.Faction)); } break; } @@ -365,14 +376,36 @@ namespace Barotrauma public float GetCommonness(Level level) { - Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty; - return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; + if (level.GenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness)) + { + return generationParamsCommonness; + } + else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness)) + { + return startOutpostParamsCommonness; + } + else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness)) + { + return endOutpostParamsCommonness; + } + return DefaultCommonness; } public int GetEventCount(Level level) { - if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; } - return count; + int finishedEventCount = 0; + if (level is not null) + { + level.LevelData.FinishedEvents.TryGetValue(this, out finishedEventCount); + } + if (level.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) + { + return eventCount - finishedEventCount; + } + return count - finishedEventCount; } public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index ced3da5c8..58d243d43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -28,6 +27,8 @@ namespace Barotrauma private const float EndDelay = 5.0f; private float endTimer; + private bool allowOrderingRescuees; + public override bool AllowRespawn => false; public override bool AllowUndocking @@ -39,17 +40,17 @@ namespace Barotrauma } } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0) + if (State == 0) { - return Enumerable.Empty(); + return Targets.Select(t => (Prefab.SonarLabel, t.WorldPosition)); } else { - return Targets.Select(t => t.WorldPosition); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } } @@ -83,6 +84,8 @@ namespace Barotrauma { characterConfig = prefab.ConfigElement.GetChildElement("Characters"); + allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true); + string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); @@ -144,10 +147,7 @@ namespace Barotrauma ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPoint == null) - { - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Vector2 spawnPos = spawnPoint.WorldPosition; if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) { @@ -186,7 +186,12 @@ namespace Barotrauma if (element.Attribute("identifier") != null && element.Attribute("from") != null) { - HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found"); + continue; + } for (int i = 0; i < count; i++) { LoadHuman(humanPrefab, element, submarine); @@ -198,7 +203,7 @@ namespace Barotrauma var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found"); continue; } for (int i = 0; i < count; i++) @@ -214,19 +219,25 @@ namespace Barotrauma { Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); + var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( - SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, + SpawnAction.SpawnLocationType.Outpost, spawnPointType, moduleFlags ?? humanPrefab.GetModuleFlags(), spawnPointTags ?? humanPrefab.GetSpawnPointTags(), element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); bool requiresRescue = element.GetAttributeBool("requirerescue", false); - - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos); + var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos); + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag); + } + } if (spawnPos is WayPoint wp) { @@ -237,9 +248,19 @@ namespace Barotrauma { requireRescue.Add(spawnedCharacter); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + } #endif } + else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi) + { + var order = OrderPrefab.Prefabs["fightintruders"] + .CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false); + } if (element.GetAttributeBool("requirekill", false)) { @@ -252,10 +273,7 @@ namespace Barotrauma Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); characters.Add(spawnedCharacter); if (element.GetAttributeBool("requirekill", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 2bfb34391..1a5a8cb3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -18,17 +18,19 @@ namespace Barotrauma private Ruin TargetRuin { get; set; } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State == 0) { - return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition); + return allTargets + .Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))) + .Select(t => (Prefab.SonarLabel, t.WorldPosition)); } else { - return Enumerable.Empty(); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } } @@ -164,7 +166,7 @@ namespace Barotrauma { bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : - Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit); + Submarine.MainSub is { } sub && sub.AtEitherExit; return State > 0 && exitingLevel; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 634652ffa..2350443aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -69,15 +69,7 @@ namespace Barotrauma } } - public override LocalizedString SonarLabel - { - get - { - return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel; - } - } - - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { @@ -85,7 +77,12 @@ namespace Barotrauma { yield break; } - yield return level.BeaconStation.WorldPosition; + else + { + yield return ( + Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, + level.BeaconStation.WorldPosition); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..daa064131 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -0,0 +1,295 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + enum MissionPhase + { + Initial, + NoItemsDestroyed, + SomeItemsDestroyed, + AllItemsDestroyed, + BossKilled + } + + private readonly CharacterPrefab bossPrefab; + private readonly CharacterPrefab minionPrefab; + + private readonly Identifier spawnPointTag; + private readonly Identifier destructibleItemTag; + + private readonly string endCinematicSound; + + private ImmutableArray minions; + private readonly int minionCount; + private readonly float minionScatter; + + private Character boss; + + private readonly ItemPrefab projectilePrefab; + + private float projectileTimer = 30.0f; + + private readonly float startCinematicDistance = 30.0f; + + private float endCinematicTimer; + + private readonly List destructibleItems = new List(); + + protected readonly float wakeUpCinematicDelay = 5.0f; + protected readonly float bossWakeUpDelay = 7.0f; + protected readonly float cameraWaitDuration = 7.0f; + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get { return destructibleItems.Where(it => it.Condition > 0.0f).Select(it => (Prefab.SonarLabel, it.WorldPosition)); } + } + + public override int State + { + get { return base.State; } + set + { + + if (state != value) + { + base.State = value; + OnStateChangedProjSpecific(); + if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.Invoke(() => + { + if (boss != null && !boss.Removed) + { + boss.AnimController.ColliderIndex = 1; + } + }, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2); + } + } + } + } + + private MissionPhase Phase + { + get + { + //state 0: nothing happens yet, play a cinematic and skip to the next state when close enough to the boss + //state 1: start cinematic played + //state 2: first destructibleItems destroyed + //state 3: 2nd destructibleItems destroyed + //state 4: all destructibleItems destroyed + //state 5: boss killed + if (state == 0) { return MissionPhase.Initial; } + if (state == 1) { return MissionPhase.NoItemsDestroyed; } + if (state < destructibleItems.Count + 1) { return MissionPhase.SomeItemsDestroyed; } + if (state < destructibleItems.Count + 2) { return MissionPhase.AllItemsDestroyed; } + return MissionPhase.BossKilled; + } + } + + public EndMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) + { + Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("bossfile", Identifier.Empty); + if (!speciesName.IsEmpty) + { + bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (bossPrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + else + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set."); + } + + Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty); + if (!minionName.IsEmpty) + { + minionPrefab = CharacterPrefab.FindBySpeciesName(minionName); + if (minionPrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + + minionCount = Math.Min(prefab.ConfigElement.GetAttributeInt(nameof(minionCount), 0), 255); + minionScatter = Math.Min(prefab.ConfigElement.GetAttributeFloat(nameof(minionScatter), 0), 10000); + + Identifier projectileId = prefab.ConfigElement.GetAttributeIdentifier("projectile", Identifier.Empty); + if (!projectileId.IsEmpty) + { + projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab; + if (projectilePrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\"."); + } + } + + spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty); + destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty); + endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty); + startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0); + } + + protected override void StartMissionSpecific(Level level) + { + var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); + if (spawnPoint == null) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); + return; + } + if (!IsClient) + { + boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); + var minionList = new List(); + float angle = 0; + float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1); + for (int i = 0; i < minionCount; i++) + { + minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); + angle += angleStep; + } + SwarmBehavior.CreateSwarm(minionList.Cast()); + minions = minionList.ToImmutableArray(); + } + if (destructibleItemTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set."); + return; + } + destructibleItems.Clear(); + destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag))); + if (destructibleItems.None()) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\"."); + return; + } + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + UpdateProjSpecific(); + + if (state == 0) + { + if (startCinematicDistance <= 0.0f || + boss == null || Submarine.MainSub == null || + Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, boss.WorldPosition) <= startCinematicDistance * startCinematicDistance) + { + State = 1; + } + return; + } + + if (!IsClient && State > 0) + { + State = Math.Max(State, destructibleItems.Count(it => it.Condition <= 0.0f) + 1); + } + + if (Phase == MissionPhase.AllItemsDestroyed) + { + if (projectilePrefab != null && boss != null && !boss.IsDead && !boss.Removed) + { + projectileTimer -= deltaTime; + if (projectileTimer <= 0.0f) + { + float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition); + float distanceFactor = Math.Min(dist / 10000.0f, 1.0f); + int projectileAmount = Rand.Range(3, 6); + //more concentrated shots the further the sub is + float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f); + for (int i = 0; i < projectileAmount; i++) + { + int index = i; + Entity.Spawner.AddItemToSpawnQueue(projectilePrefab, boss.WorldPosition, onSpawned: it => + { + var projectile = it.GetComponent(); + float angle = MathUtils.VectorToAngle(Submarine.MainSub.WorldPosition - boss.WorldPosition); + if (projectileAmount > 1) + { + angle += (index / (float)(projectileAmount - 1) - 0.5f) * spread; + } + it.body.SetTransform(it.SimPosition, angle); + it.UpdateTransform(); + //faster launch velocity the further the sub is + projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor)); + }); + } + + //the closer the sub is, more likely it is to shoot frequently + float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor); + if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability) + { + projectileTimer = Rand.Range(3.0f, 5.0f); + } + else + { + projectileTimer = Rand.Range(15f, 30f); + } + } + } + else + { + State = Math.Max(destructibleItems.Count + 2, State); + } + } + else if (Phase == MissionPhase.BossKilled) + { + const float EndCinematicDuration = 20.0f; + + endCinematicTimer += deltaTime; +#if CLIENT + Screen.Selected.Cam.Shake = MathHelper.Clamp(MathF.Pow(endCinematicTimer, 3), 5.0f, 200.0f); + + + Screen.Selected.Cam.Rotation = + Math.Max((endCinematicTimer - 5.0f) * 0.05f, 0.0f) + + (PerlinNoise.GetPerlin(endCinematicTimer * 0.1f, endCinematicTimer * 0.05f) - 0.5f) * 0.5f * (endCinematicTimer / EndCinematicDuration); + if (Rand.Range(0.0f, 100.0f) < endCinematicTimer) + { + Level.Loaded.Renderer.Flash(); + } + Level.Loaded.Renderer.ChromaticAberrationStrength = endCinematicTimer * 5; + Level.Loaded.Renderer.CollapseEffectOrigin = boss.WorldPosition; + Level.Loaded.Renderer.CollapseEffectStrength = endCinematicTimer / EndCinematicDuration; +#endif + if (endCinematicTimer > 5 && !IsClient) + { + foreach (Character c in Character.CharacterList) + { + if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null) + { + c.SetAllDamage(200.0f, 0.0f, 0.0f); + } + } + } + + if (endCinematicTimer > EndCinematicDuration && !IsClient) + { + //endCinematicTimer = 0; + GameMain.GameSession.Campaign?.LoadNewLevel(); + } + } + + } + + partial void UpdateProjSpecific(); + + partial void OnStateChangedProjSpecific(); + + protected override bool DetermineCompleted() + { + return Phase == MissionPhase.BossKilled; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index fe7c3ff84..51dd38d21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -10,11 +10,12 @@ namespace Barotrauma { partial class EscortMission : Mission { - private readonly XElement characterConfig; - private readonly XElement itemConfig; + private readonly ContentXElement characterConfig; + private readonly ContentXElement itemConfig; private readonly List characters = new List(); private readonly Dictionary> characterItems = new Dictionary>(); + private readonly Dictionary> characterStatusEffects = new Dictionary>(); private readonly int baseEscortedCharacters; private readonly float scalingEscortedCharacters; @@ -28,7 +29,8 @@ namespace Barotrauma private readonly List terroristCharacters = new List(); private bool terroristsShouldAct = false; private float terroristDistanceSquared; - private const string TerroristTeamChangeIdentifier = "terrorist"; + private const string TerroristTeamChangeIdentifier = "terrorist"; + private readonly string terroristAnnounceDialogTag = string.Empty; public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) @@ -39,6 +41,7 @@ namespace Barotrauma scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems"); + terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty); CalculateReward(); } @@ -96,14 +99,27 @@ namespace Barotrauma } List humanPrefabsToSpawn = new List(); - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement characterElement in characterConfig.Elements()) { int count = CalculateScalingEscortedCharacterCount(inMission: true); - var humanPrefab = GetHumanPrefabFromElement(element); + var humanPrefab = GetHumanPrefabFromElement(characterElement); for (int i = 0; i < count; i++) { humanPrefabsToSpawn.Add(humanPrefab); } + foreach (var element in characterElement.Elements()) + { + if (element.NameAsIdentifier() == "statuseffect") + { + var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value); + if (newEffect == null) { continue; } + if (!characterStatusEffects.ContainsKey(humanPrefab)) + { + characterStatusEffects[humanPrefab] = new List { newEffect }; + } + characterStatusEffects[humanPrefab].Add(newEffect); + } + } } //if any of the escortees have a job defined, try to use a spawnpoint designated for that job @@ -128,6 +144,13 @@ namespace Barotrauma { humanAI.InitMentalStateManager(); } + if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList)) + { + foreach (var statusEffect in statusEffectList) + { + statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter); + } + } } @@ -162,7 +185,7 @@ namespace Barotrauma } int i = 0; - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement element in characterConfig.Elements()) { string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty); string colorIdentifier = element.GetAttributeString("color", string.Empty); @@ -231,7 +254,10 @@ namespace Barotrauma if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands) { character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true)); - character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + if (!string.IsNullOrEmpty(terroristAnnounceDialogTag)) + { + character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + } XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index f0fa3a328..a1924db58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using System; + +namespace Barotrauma { partial class GoToMission : Mission { @@ -11,7 +13,7 @@ { if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { - State = 1; + State = Math.Max(1, State); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 0d136f64f..90ac22989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -11,20 +11,7 @@ namespace Barotrauma { partial class MineralMission : Mission { - private struct ResourceCluster - { - public int Amount; - public float Rotation; - - public ResourceCluster(int amount, float rotation) - { - Amount = amount; - Rotation = rotation; - } - - public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation); - } - private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary resourceAmounts = new Dictionary(); private readonly Dictionary> spawnedResources = new Dictionary>(); private readonly Dictionary relevantLevelResources = new Dictionary(); private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>(); @@ -50,13 +37,13 @@ namespace Barotrauma /// private readonly float resourceHandoverAmount; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { return missionClusterPositions - .Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1])) - .Select(p => p.Item2); + .Where(p => spawnedResources.ContainsKey(p.Identifier) && AnyAreUncollected(spawnedResources[p.Identifier])) + .Select(p => (ModifyMessage(Prefab.SonarLabel, color: false), p.Position)); } } @@ -64,7 +51,6 @@ namespace Barotrauma public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage); public override LocalizedString Description => ModifyMessage(description); public override LocalizedString Name => ModifyMessage(base.Name, false); - public override LocalizedString SonarLabel => ModifyMessage(base.SonarLabel, false); public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { @@ -82,13 +68,13 @@ namespace Barotrauma { var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { continue; } - if (resourceClusters.ContainsKey(identifier)) + if (resourceAmounts.ContainsKey(identifier)) { - resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation); + resourceAmounts[identifier]++; } else { - resourceClusters.Add(identifier, (1, 0.0f)); + resourceAmounts.Add(identifier, 1); } } } @@ -129,7 +115,7 @@ namespace Barotrauma if (IsClient) { return; } - foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + foreach ((Identifier identifier, int amount) in resourceAmounts) { if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { @@ -137,10 +123,10 @@ namespace Barotrauma continue; } - var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves); - if (spawnedResources.Count < cluster.Amount) + var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves); + if (spawnedResources.Count < amount) { - DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}"); + DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}"); } if (spawnedResources.None()) { continue; } @@ -175,7 +161,7 @@ namespace Barotrauma State = 1; break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } @@ -195,7 +181,7 @@ namespace Barotrauma { // When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission) var handoverResources = new List(); - foreach (Identifier identifier in resourceClusters.Keys) + foreach (Identifier identifier in resourceAmounts.Keys) { if (relevantLevelResources.TryGetValue(identifier, out var availableResources)) { @@ -232,11 +218,11 @@ namespace Barotrauma private void FindRelevantLevelResources() { relevantLevelResources.Clear(); - foreach (var identifier in resourceClusters.Keys) + foreach (var identifier in resourceAmounts.Keys) { var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier && i.Submarine == null && i.ParentInventory == null && - (!(i.GetComponent() is Holdable h) || (h.Attachable && h.Attached))) + (i.GetComponent() is not Holdable h || (h.Attachable && h.Attached))) .ToArray(); relevantLevelResources.Add(identifier, items); } @@ -244,12 +230,12 @@ namespace Barotrauma private bool EnoughHaveBeenCollected() { - foreach (var kvp in resourceClusters) + foreach (var kvp in resourceAmounts) { if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { var collected = availableResources.Count(HasBeenCollected); - var needed = kvp.Value.Amount; + var needed = kvp.Value; if (collected < needed) { return false; } } else @@ -300,10 +286,10 @@ namespace Barotrauma protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true) { int i = 1; - foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + foreach ((Identifier identifier, int amount) in resourceAmounts) { Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? ""); - Replace($"[resourcequantity{i}]", cluster.Amount.ToString()); + Replace($"[resourcequantity{i}]", amount.ToString()); i++; } Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 0ccb6fd13..c0f125362 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -22,7 +22,7 @@ namespace Barotrauma public virtual int State { get { return state; } - protected set + set { if (state != value) { @@ -30,6 +30,11 @@ namespace Barotrauma TryTriggerEvents(state); #if SERVER GameMain.Server?.UpdateMissionState(this); +#elif CLIENT + if (Prefab.ShowProgressBar) + { + CharacterHUD.ShowMissionProgressBar(this); + } #endif ShowMessage(State); OnMissionStateChanged?.Invoke(this); @@ -37,6 +42,8 @@ namespace Barotrauma } } + public int TimesAttempted { get; set; } + protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; private readonly CheckDataAction completeCheckDataAction; @@ -44,6 +51,12 @@ namespace Barotrauma public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; + /// + /// The reward that was actually given from completing the mission, taking any talent bonuses into account + /// (some of which may not be possible to determine in advance) + /// + private int? finalReward; + public virtual LocalizedString Name => Prefab.Name; private readonly LocalizedString successMessage; @@ -113,15 +126,19 @@ namespace Barotrauma get { return null; } } - public virtual IEnumerable SonarPositions + public virtual IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { - get { return Enumerable.Empty(); } + get { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } - public virtual LocalizedString SonarLabel => Prefab.SonarLabel; - public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; + /// + /// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission. + /// Defaults to Locations[0] + /// + public Location OriginLocation; + public readonly Location[] Locations; public int? Difficulty @@ -141,7 +158,7 @@ namespace Barotrauma } } - private List delayedTriggerEvents = new List(); + private readonly List delayedTriggerEvents = new List(); public Action OnMissionStateChanged; @@ -157,12 +174,13 @@ namespace Barotrauma Headers = prefab.Headers; var messages = prefab.Messages.ToArray(); + OriginLocation = locations[0]; Locations = locations; var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction)); if (endConditionElement != null) { - completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})"); + completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})"); } for (int n = 0; n < 2; n++) @@ -307,7 +325,7 @@ namespace Barotrauma private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger) { if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } - if (trigger.Delay > 0) + if (trigger.Delay > 0 || trigger.State == 0) { if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger)) { @@ -357,6 +375,8 @@ namespace Barotrauma GiveReward(); } + TimesAttempted++; + EndMissionSpecific(completed); } @@ -364,6 +384,27 @@ namespace Barotrauma protected virtual void EndMissionSpecific(bool completed) { } + /// + /// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly. + /// + public int GetFinalReward(Submarine sub) + { + return finalReward ?? GetReward(sub); + } + + /// + /// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney, + /// and should only be called once when the mission is completed! + /// + private void CalculateFinalReward(Submarine sub) + { + int reward = GetReward(sub); + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + finalReward = (int)(reward * missionMoneyGainMultiplier.Value); + } private void GiveReward() { @@ -407,39 +448,35 @@ namespace Barotrauma info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); } - // apply money gains afterwards to prevent them from affecting XP gains - var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); - crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - - int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); - + CalculateFinalReward(Submarine.MainSub); #if SERVER - totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); + finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value); #endif bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; - if (isSingleplayerOrServer && totalReward > 0) + if (isSingleplayerOrServer) { - campaign.Bank.Give(totalReward); - } - - foreach (Character character in crewCharacters) - { - character.Info.MissionsCompletedSinceDeath++; - } - - foreach (KeyValuePair reputationReward in ReputationRewards) - { - if (reputationReward.Key == "location") + if (finalReward > 0) { - Locations[0].Reputation.AddReputation(reputationReward.Value); - Locations[1].Reputation.AddReputation(reputationReward.Value); + campaign.Bank.Give(finalReward.Value); } - else + + foreach (Character character in crewCharacters) { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); - if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } + character.Info.MissionsCompletedSinceDeath++; + } + + foreach (KeyValuePair reputationReward in ReputationRewards) + { + if (reputationReward.Key == "location") + { + OriginLocation.Reputation?.AddReputation(reputationReward.Value); + } + else + { + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); + float prevValue = faction.Reputation.Value; + faction?.Reputation.AddReputation(reputationReward.Value); + } } } @@ -484,18 +521,15 @@ namespace Barotrauma float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); - return reward switch - { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), - None _ => (0, rewardPercentage, sum), - _ => throw new ArgumentOutOfRangeException() - }; + int amount = reward.TryUnwrap(out var a) ? a : 0; + + return ((int)(amount * rewardWeight), rewardPercentage, sum); } protected void ChangeLocationType(LocationTypeChange change) { if (change == null) { throw new ArgumentException(); } - if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) + if (GameMain.GameSession.GameMode is CampaignMode campaign && !IsClient) { int srcIndex = -1; for (int i = 0; i < Locations.Length; i++) @@ -509,13 +543,15 @@ namespace Barotrauma if (srcIndex == -1) { return; } var location = Locations[srcIndex]; + if (location.LocationTypeChangesBlocked) { return; } + if (change.RequiredDurationRange.X > 0) { location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab); } else { - location.ChangeType(LocationType.Prefabs[change.ChangeToType]); + location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]); location.LocationTypeChangeCooldown = change.CooldownAfterChange; } } @@ -529,7 +565,6 @@ namespace Barotrauma if (element.Attribute("name") != null) { DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); - return null; } @@ -538,7 +573,7 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); return null; } @@ -557,8 +592,7 @@ namespace Barotrauma Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); spawnedCharacter.HumanPrefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); - humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false); - + humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false); characters.Add(spawnedCharacter); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 92f08baec..90e4d007b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -25,7 +25,8 @@ namespace Barotrauma GoTo = 0x400, ScanAlienRuins = 0x800, ClearAlienRuins = 0x1000, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins + End = 0x2000, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End } partial class MissionPrefab : PrefabWithUintIdentifier @@ -45,14 +46,15 @@ namespace Barotrauma { MissionType.Pirate, typeof(PirateMission) }, { MissionType.GoTo, typeof(GoToMission) }, { MissionType.ScanAlienRuins, typeof(ScanMission) }, - { MissionType.ClearAlienRuins, typeof(AlienRuinMission) } + { MissionType.ClearAlienRuins, typeof(AlienRuinMission) }, + { MissionType.End, typeof(EndMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { { MissionType.Combat, typeof(CombatMission) } }; - public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; + public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo, MissionType.End }; private readonly ConstructorInfo constructor; @@ -62,11 +64,7 @@ namespace Barotrauma public readonly Identifier TextIdentifier; - private readonly string[] tags; - public IEnumerable Tags - { - get { return tags; } - } + public readonly ImmutableHashSet Tags; public readonly LocalizedString Name; public readonly LocalizedString Description; @@ -93,10 +91,24 @@ namespace Barotrauma public readonly bool AllowRetry; + public readonly bool ShowInMenus, ShowStartMessage; + public readonly bool IsSideObjective; + public readonly bool AllowOtherMissionsInLevel; + public readonly bool RequireWreck, RequireRuin; + /// + /// If enabled, locations this mission takes place in cannot change their type + /// + public readonly bool BlockLocationTypeChanges; + + public readonly bool ShowProgressBar; + public readonly bool ShowProgressInNumbers; + public readonly int MaxProgressState; + public readonly LocalizedString ProgressBarLabel; + /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// @@ -144,7 +156,7 @@ namespace Barotrauma TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); - tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + Tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); string nameTag = element.GetAttributeString("name", ""); Name = TextManager.Get($"MissionName.{TextIdentifier}"); @@ -167,16 +179,26 @@ namespace Barotrauma Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); + ShowInMenus = element.GetAttributeBool("showinmenus", true); + ShowStartMessage = element.GetAttributeBool("showstartmessage", true); IsSideObjective = element.GetAttributeBool("sideobjective", false); RequireWreck = element.GetAttributeBool("requirewreck", false); RequireRuin = element.GetAttributeBool("requireruin", false); + BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); Commonness = element.GetAttributeInt("commonness", 1); + AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true); if (element.GetAttribute("difficulty") != null) { int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } + ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); + ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); + MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1); + string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), ""); + ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel); + string successMessageTag = element.GetAttributeString("successmessage", ""); SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}"); if (!string.IsNullOrEmpty(successMessageTag)) @@ -350,6 +372,7 @@ namespace Barotrauma { return AllowedLocationTypes.Any(lt => lt == "any") || + AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) || AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } @@ -357,11 +380,11 @@ namespace Barotrauma { if (fromType == "any" || fromType == from.Type.Identifier || - (fromType == "anyoutpost" && from.HasOutpost())) + (fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned")) { if (toType == "any" || toType == to.Type.Identifier || - (toType == "anyoutpost" && to.HasOutpost())) + (toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned")) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 9b3641502..a9ab792ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -16,17 +16,20 @@ namespace Barotrauma private readonly Level.PositionType spawnPosType; private Vector2? spawnPos = null; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { - return Enumerable.Empty(); + yield break; } else { - return sonarPositions; + foreach (Vector2 sonarPos in sonarPositions) + { + yield return (Prefab.SonarLabel, sonarPos); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index bc71d49dc..e849a5499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -31,17 +31,17 @@ namespace Barotrauma private Vector2 nestPosition; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { - Enumerable.Empty(); + yield break; } else { - yield return nestPosition; + yield return (Prefab.SonarLabel, nestPosition); } } } @@ -260,9 +260,25 @@ namespace Barotrauma int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); for (int i = 0; i < amount; i++) { - Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true); + Vector2 offsetPosition; + int tries = 0; + do + { + offsetPosition = nestPosition + Rand.Vector(100.0f); + tries++; + if (tries > 10) + { + offsetPosition = nestPosition; + break; + } + } while (Level.Loaded.IsPositionInsideWall(offsetPosition)); + Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true); } } + if (Level.Loaded.IsPositionInsideWall(nestPosition)) + { + DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition})."); + } monsterPrefabs.Clear(); break; } @@ -274,7 +290,7 @@ namespace Barotrauma break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index ff33e7ec9..ecc29fcb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -36,23 +36,32 @@ namespace Barotrauma private readonly List patrolPositions = new List(); - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - var empty = Enumerable.Empty(); - if (outsideOfSonarRange) + if (!outsideOfSonarRange || state > 1) { - return State switch - { - 0 => patrolPositions, - 1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty, - _ => empty, - }; + yield break; + } - else + else if (state == 0) { - return empty; + foreach (Vector2 patrolPos in patrolPositions) + { + yield return (Prefab.SonarLabel, patrolPos); + } + } + else if (state == 1) + { + if (lastSighting.HasValue) + { + yield return (Prefab.SonarLabel, lastSighting.Value); + } + else + { + yield break; + } } } } @@ -85,6 +94,31 @@ namespace Barotrauma characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes"); addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); + //make sure all referenced character types are defined + foreach (XElement characterElement in characterConfig.Elements()) + { + var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty); + var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId); + if (characterTypeElement == null) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\"."); + } + } + //make sure all defined character types can be found from human prefabs + foreach (XElement characterTypeElement in characterTypeConfig.Elements()) + { + foreach (XElement characterElement in characterTypeElement.Elements()) + { + Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty); + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); + } + } + } + // for campaign missions, set level at construction LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData; if (levelData != null) @@ -100,6 +134,7 @@ namespace Barotrauma //level already set return; } + submarineInfo = null; levelData = level; missionDifficulty = level?.Difficulty ?? 0; @@ -117,8 +152,15 @@ namespace Barotrauma DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); return; } - // maybe a little redundant - var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(x => x.Path == submarinePath); + + BaseSubFile contentFile = + GetSubFile(submarinePath) ?? + GetSubFile(submarinePath); + BaseSubFile GetSubFile(ContentPath path) where T : BaseSubFile + { + return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(f => f.Path == submarinePath); + } + if (contentFile == null) { DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); @@ -241,9 +283,10 @@ namespace Barotrauma // it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional // if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand); + var characterId = element.GetAttributeString("typeidentifier", string.Empty); for (int i = 0; i < amountCreated; i++) { - XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault(); + XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault(); if (characterType == null) { @@ -253,7 +296,10 @@ namespace Barotrauma XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); - Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null); + var humanPrefab = GetHumanPrefabFromElement(variantElement); + if (humanPrefab == null) { continue; } + + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null); if (!commanderAssigned) { bool isCommander = variantElement.GetAttributeBool("iscommander", false); @@ -305,8 +351,9 @@ namespace Barotrauma if (enemySub == null) { - DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined."); - // TODO: should we set the state to something here? + DebugConsole.ThrowError(submarineInfo == null ? + $"Error in PirateMission: enemy sub was not created (submarineInfo == null)." : + $"Error in PirateMission: enemy sub was not created."); return; } @@ -345,10 +392,11 @@ namespace Barotrauma protected override void UpdateMissionSpecific(float deltaTime) { - if (state >= 2) { return; } + if (state >= 2 || enemySub == null) { return; } float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange); outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange; + if (CheckWinState()) { State = 2; @@ -411,6 +459,7 @@ namespace Barotrauma characters.Clear(); characterItems.Clear(); failed = !completed; + submarineInfo = null; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 0d1b41b98..3a0b83978 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -5,40 +5,182 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { partial class SalvageMission : Mission { - private readonly ItemPrefab itemPrefab; - private Item item; - - private readonly Level.PositionType spawnPositionType; - - private readonly string containerTag; - - private readonly string existingItemTag; - - private readonly bool showMessageWhenPickedUp; - - /// - /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. - /// - private readonly List> statusEffects = new List>(); - - public override IEnumerable SonarPositions + private class Target { - get + public Item Item; + + /// + /// Note that the integer values matter here: the state of the target can't go back to a smaller value, + /// and a larger or equal value than the RequiredRetrievalState means the item counts as retrieved + /// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub) + /// + public enum RetrievalState + { + None = 0, + Interact = 1, + PickedUp = 2, + RetrievedToSub = 3 + } + + public readonly ItemPrefab ItemPrefab; + public readonly Level.PositionType SpawnPositionType; + public readonly string ContainerTag; + public readonly string ExistingItemTag; + + public readonly bool RemoveItem; + + public readonly LocalizedString SonarLabel; + + public readonly bool AllowContinueBeforeRetrieved; + + /// + /// Does the target need to be picked up or brought to the sub for mission to be considered successful. + /// If None, the target has no effect on the completion of the mission. + /// + public readonly RetrievalState RequiredRetrievalState; + + public readonly bool HideLabelAfterRetrieved; + + public bool Retrieved { - if (item == null) + get { - Enumerable.Empty(); + return RequiredRetrievalState switch + { + RetrievalState.None => true, + RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState, + RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub, + _ => throw new NotImplementedException(), + }; + } + } + + private RetrievalState state; + public RetrievalState State + { + get { return state; } + set + { + if (value == state) { return; } + state = value; +#if SERVER + GameMain.Server?.UpdateMissionState(mission); +#endif + } + } + + public bool Interacted; + + private readonly SalvageMission mission; + + /// + /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. + /// + public readonly List> StatusEffects = new List>(); + + public Target(ContentXElement element, SalvageMission mission) + { + this.mission = mission; + ContainerTag = element.GetAttributeString("containertag", ""); + RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); + AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); + HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); + + string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); + if (!string.IsNullOrEmpty(sonarLabelTag)) + { + SonarLabel = + TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") + .Fallback(TextManager.Get(sonarLabelTag)) + .Fallback(element.GetAttributeString("sonarlabel", "")); + } + ExistingItemTag = element.GetAttributeString("existingitemtag", ""); + + RemoveItem = element.GetAttributeBool("removeitem", true); + + if (element.GetAttribute("itemname") != null) + { + DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); + string itemName = element.GetAttributeString("itemname", ""); + ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); + } } else { - yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition; + Identifier itemIdentifier = element.GetAttributeIdentifier("itemidentifier", Identifier.Empty); + if (!itemIdentifier.IsEmpty) + { + ItemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; + } + if (ItemPrefab == null) + { + string itemTag = element.GetAttributeString("itemtag", ""); + ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; + } + if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); + } + } + + SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + { + var newEffect = StatusEffect.Load(subElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Add(new List { newEffect }); + break; + } + case "chooserandom": + StatusEffects.Add(new List()); + foreach (var effectElement in subElement.Elements()) + { + var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Last().Add(newEffect); + } + break; + } + } + } + + public void Reset() + { + state = RetrievalState.None; + Item = null; + } + } + + private readonly List targets = new List(); + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get + { + foreach (var target in targets) + { + if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; } + if (target.Item != null) + { + yield return ( + target.SonarLabel ?? Prefab.SonarLabel, + target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition); + } + if (!target.AllowContinueBeforeRetrieved && !target.Retrieved) { break; } } } } @@ -46,225 +188,254 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); - - if (prefab.ConfigElement.GetAttribute("itemname") != null) + foreach (ContentXElement subElement in prefab.ConfigElement.Elements()) { - DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); - string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (itemPrefab == null) + if (subElement.NameAsIdentifier() == "target") { - DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName); + targets.Add(new Target(subElement, this)); } } - else + if (!targets.Any()) { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); - if (itemIdentifier != null) - { - itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; - } - if (itemPrefab == null) - { - string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", ""); - itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; - } - if (itemPrefab == null) - { - DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier); - } - } - - existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", ""); - showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false); - - string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); - if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || - !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) - { - spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; - } - - foreach (var element in prefab.ConfigElement.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "statuseffect": - { - var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value); - if (newEffect == null) { continue; } - statusEffects.Add(new List { newEffect }); - break; - } - case "chooserandom": - statusEffects.Add(new List()); - foreach (var subElement in element.Elements()) - { - var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value); - if (newEffect == null) { continue; } - statusEffects.Last().Add(newEffect); - } - break; - } + targets.Add(new Target(prefab.ConfigElement, this)); } } protected override void StartMissionSpecific(Level level) { #if SERVER - originalInventoryID = Entity.NullEntityID; + spawnInfo.Clear(); #endif - item = null; - if (!IsClient) + foreach (var target in targets) { - //ruin/cave/wreck items are allowed to spawn close to the sub - float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ? - 0.0f : Level.Loaded.Size.X * 0.3f; - Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); - - if (!string.IsNullOrEmpty(existingItemTag)) + bool usedExistingItem = false; + UInt16 originalInventoryID = 0; + byte originalItemContainerIndex = 0; + int originalSlotIndex = 0; + var executedEffectIndices = new List<(int listIndex, int effectIndex)>(); + + target.Reset(); + if (!IsClient) { - var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag)); - switch (spawnPositionType) + //ruin/cave/wreck items are allowed to spawn close to the sub + float minDistance = target.SpawnPositionType switch { - case Level.PositionType.Cave: - case Level.PositionType.MainPath: - case Level.PositionType.SidePath: - item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); - break; - case Level.PositionType.Ruin: - case Level.PositionType.Wreck: - foreach (Item it in suitableItems) - { - if (it.Submarine?.Info == null) { continue; } - if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } - if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } - Rectangle worldBorders = it.Submarine.Borders; - worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); - if (Submarine.RectContains(worldBorders, it.WorldPosition)) - { - item = it; -#if SERVER - usedExistingItem = true; -#endif - break; - } - } - break; - } - } + Level.PositionType.Ruin or + Level.PositionType.Cave or + Level.PositionType.Wreck or + Level.PositionType.Outpost => 0.0f, + _ => Level.Loaded.Size.X * 0.3f, + }; + Vector2 position = + target.SpawnPositionType == Level.PositionType.None ? + Vector2.Zero : + Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f); - if (item == null) - { - item = new Item(itemPrefab, position, null); - item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation); - item.body.FarseerBody.BodyType = BodyType.Kinematic; - } - - for (int i = 0; i < statusEffects.Count; i++) - { - List effectList = statusEffects[i]; - if (effectList.Count == 0) { continue; } - int effectIndex = Rand.Int(effectList.Count); - var selectedEffect = effectList[effectIndex]; - item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); -#if SERVER - executedEffectIndices.Add(new Pair(i, effectIndex)); -#endif - } - - //try to find a container and place the item inside it - if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) - { - List validContainers = new List(); - foreach (Item it in Item.ItemList) + if (!string.IsNullOrEmpty(target.ExistingItemTag)) { - if (!it.HasTag(containerTag)) { continue; } - if (!it.IsPlayerTeamInteractable) { continue; } - switch (spawnPositionType) + var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag)); + if (GameMain.GameSession?.Missions != null) + { + //don't choose an item that was already chosen as the target for another salvage mission + suitableItems = suitableItems.Where(it => + GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it))); + } + switch (target.SpawnPositionType) { case Level.PositionType.Cave: case Level.PositionType.MainPath: - if (it.Submarine != null) { continue; } + case Level.PositionType.SidePath: + target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); +#if SERVER + usedExistingItem = target.Item != null; +#endif break; case Level.PositionType.Ruin: - if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } - break; case Level.PositionType.Wreck: - if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + case Level.PositionType.Outpost: + foreach (Item it in suitableItems) + { + if (it.Submarine?.Info == null) { continue; } + if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } + if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; } + Rectangle worldBorders = it.Submarine.Borders; + worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, it.WorldPosition)) + { + target.Item = it; +#if SERVER + usedExistingItem = true; +#endif + break; + } + } + break; + default: + target.Item = suitableItems.FirstOrDefault(); +#if SERVER + usedExistingItem = target.Item != null; +#endif break; } - var itemContainer = it.GetComponent(); - if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); } } - if (validContainers.Any()) + + if (target.Item == null) { - var selectedContainer = validContainers.GetRandomUnsynced(); - if (selectedContainer.Combine(item, user: null)) + if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag)) { + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}"); + continue; + } + target.Item = new Item(target.ItemPrefab, position, null); + target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; + } + else if (target.RequiredRetrievalState == Target.RetrievalState.Interact) + { + target.Item.OnInteract += () => + { + target.Interacted = true; + }; + } + for (int i = 0; i < target.StatusEffects.Count; i++) + { + List effectList = target.StatusEffects[i]; + if (effectList.Count == 0) { continue; } + int effectIndex = Rand.Int(effectList.Count); + var selectedEffect = effectList[effectIndex]; + target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); #if SERVER - originalInventoryID = selectedContainer.Item.ID; - originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); - originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1; + executedEffectIndices.Add((i, effectIndex)); #endif - } // Placement successful + } + + //try to find a container and place the item inside it + if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null) + { + List validContainers = new List(); + foreach (Item it in Item.ItemList) + { + if (!it.HasTag(target.ContainerTag)) { continue; } + if (!it.IsPlayerTeamInteractable) { continue; } + switch (target.SpawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + if (it.Submarine != null) { continue; } + break; + case Level.PositionType.Ruin: + if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } + break; + case Level.PositionType.Wreck: + if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + break; + } + var itemContainer = it.GetComponent(); + if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); } + } + if (validContainers.Any()) + { + var selectedContainer = validContainers.GetRandomUnsynced(); + if (selectedContainer.Combine(target.Item, user: null)) + { +#if SERVER + originalInventoryID = selectedContainer.Item.ID; + originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); + originalSlotIndex = target.Item.ParentInventory?.FindIndex(target.Item) ?? -1; +#endif + } // Placement successful + } } } +#if SERVER + spawnInfo.Add( + target, + new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices)); +#endif } } protected override void UpdateMissionSpecific(float deltaTime) { - if (item == null) + //make body dynamic when picked up + foreach (var target in targets) { -#if DEBUG - DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); -#endif - return; + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root == null) { continue; } + if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; } } - if (IsClient) + if (IsClient) { return; } + + for (int i = 0; i < targets.Count; i++) { - if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - return; - } - switch (State) - { - case 0: - if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - if (showMessageWhenPickedUp) - { - if (!(item.GetRootInventoryOwner() is Character)) { return; } - } - else - { - Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; - if (parentSub == null || parentSub.Info.Type != SubmarineType.Player) + var target = targets[i]; + if (i > 0 && !targets[i - 1].AllowContinueBeforeRetrieved && !targets[i - 1].Retrieved) { break; } + if (target.Item == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); +#endif + return; + } + switch (target.State) + { + case Target.RetrievalState.None: + if (target.Interacted) { - return; + TrySetRetrievalState(Target.RetrievalState.Interact); } - } - State = 1; - break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; - break; + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) + { + TrySetRetrievalState(Target.RetrievalState.PickedUp); + } + break; + case Target.RetrievalState.PickedUp: + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; + if (parentSub != null) + { + if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } + } + break; + } + + void TrySetRetrievalState(Target.RetrievalState retrievalState) + { + if (retrievalState < target.State) { return; } + bool wasRetrieved = false; + target.State = retrievalState; + //increment the mission state if the target became retrieved + if (!wasRetrieved && target.Retrieved) { State = i + 1; } + } + } + if (targets.All(t => t.Retrieved)) + { + State = targets.Count + 1; } } protected override bool DetermineCompleted() { - var root = item?.GetRootContainer() ?? item; - return root?.CurrentHull?.Submarine != null && (root.CurrentHull.Submarine.AtEndExit || root.CurrentHull.Submarine.AtStartExit) && !item.Removed; + return targets.All(t => t.State >= t.RequiredRetrievalState); } protected override void EndMissionSpecific(bool completed) { - item?.Remove(); - item = null; - failed = !completed && state > 0; + //consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level + failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp); + foreach (var target in targets) + { + if (target.RemoveItem) + { + target.Item?.Remove(); + target.Reset(); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index a59c69f8d..b8a2f0936 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -32,25 +32,20 @@ namespace Barotrauma } } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0) + if (State > 0 || scanTargets.None()) { - return Enumerable.Empty(); - } - else if (scanTargets.Any()) - { - return scanTargets - .Where(kvp => !kvp.Value) - .Select(kvp => kvp.Key.WorldPosition); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } else { - return Enumerable.Empty(); - } - + return scanTargets + .Where(kvp => !kvp.Value) + .Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index d4e57fc47..e45b7b4cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -244,7 +244,12 @@ namespace Barotrauma float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } + if (sub.Info.Type != SubmarineType.Player && + sub.Info.Type != SubmarineType.EnemySubmarine && + sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) + { + continue; + } float minDistToSub = GetMinDistanceToSub(sub); if (dist < minDistToSub * minDistToSub) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index cbb841a31..703aa19ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -116,7 +116,7 @@ namespace Barotrauma { foreach (Entity entity in Entity.GetEntities()) { - if (targetPredicates[tag].Any(p => p(entity))) + if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity)) { targetsToReturn.Add(entity); } @@ -131,7 +131,7 @@ namespace Barotrauma { foreach (Character npc in outpostNPCs) { - if (npc.Removed) { continue; } + if (npc.Removed || targetsToReturn.Contains(npc)) { continue; } targetsToReturn.Add(npc); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 7aa700475..272d38833 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -82,7 +82,13 @@ namespace Barotrauma.Extensions return count == 0 ? default : source.ElementAt(Rand.Range(0, count, Rand.RandSync.Unsynced)); } } - + + public static T GetRandom(this IEnumerable source, Random rand) + where T : PrefabWithUintIdentifier + { + return source.OrderBy(p => p.UintIdentifier).ToArray().GetRandom(rand); + } + public static T GetRandom(this IEnumerable source, Rand.RandSync randSync) where T : PrefabWithUintIdentifier { @@ -305,11 +311,14 @@ namespace Barotrauma.Extensions => source .Where(nullable => nullable.HasValue) .Select(nullable => nullable.Value); - + public static IEnumerable NotNone(this IEnumerable> source) - => source - .OfType>() - .Select(some => some.Value); + { + foreach (var o in source) + { + if (o.TryUnwrap(out var v)) { yield return v; } + } + } public static IEnumerable Successes( this IEnumerable> source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index 3b56fd1aa..7322cb985 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -19,5 +19,12 @@ namespace Barotrauma public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; + + public static bool IsTrueString(this string s) + => s.Length == 4 + && s[0] is 'T' or 't' + && s[1] is 'R' or 'r' + && s[2] is 'U' or 'u' + && s[3] is 'E' or 'e'; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 1a741f3b7..0560da516 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -260,10 +260,12 @@ namespace Barotrauma } bool success = false; bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; + float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) { if (preferredContainer.CampaignOnly && !isCampaign) { continue; } if (preferredContainer.NotCampaign && isCampaign) { continue; } + if (levelDifficulty < preferredContainer.MinLevelDifficulty || levelDifficulty > preferredContainer.MaxLevelDifficulty) { continue; } if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; } validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true); if (validContainers.None()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index edcf1532a..1f3535788 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -8,6 +8,8 @@ using System.Linq; using System.Text; using System.Xml.Linq; using Barotrauma.Networking; +using System.Collections; +using System.Collections.Immutable; #if SERVER using Barotrauma.Networking; #endif @@ -414,14 +416,31 @@ namespace Barotrauma } } #endif - return Submarine.MainSub.GetItems(true).FindAll(item => + return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)); + } + + public static IReadOnlyCollection FindAllItemsOnPlayerAndSub(Character character) + { + List allItems = new(); + if (character?.Inventory is { } inv) + { + allItems.AddRange(inv.FindAllItems(recursive: true)); + } + allItems.AddRange(FindAllSellableItems()); + return allItems; + } + + public static IEnumerable FindAllSellableItems() + { + if (Submarine.MainSub is null) { return Enumerable.Empty(); } + + return Submarine.MainSub.GetItems(true).FindAll(static item => { - if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } if (item.GetRootInventoryOwner() is Character) { return false; } - if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } - if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } + if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; } + if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } + if (item.GetRootContainer() is { } rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } return true; }).Distinct(); @@ -471,6 +490,21 @@ namespace Barotrauma return true; } + public static IEnumerable FindCargoRooms(IEnumerable subs) => subs.SelectMany(s => FindCargoRooms(s)); + + public static IEnumerable FindCargoRooms(Submarine sub) => WayPoint.WayPointList + .Where(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Cargo) + .Select(wp => wp.CurrentHull) + .Distinct(); + + public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) + => items.Where(it => it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + + public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => + FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) + .Select(it => it.GetComponent()) + .Where(c => c != null); + public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List availableContainers) { ItemContainer itemContainer = null; @@ -553,8 +587,8 @@ namespace Barotrauma } #endif } - - List availableContainers = new List(); + var connectedSubs = sub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + List availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList(); foreach (PurchasedItem pi in itemsToSpawn) { Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 06ab5566e..a397da922 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -248,11 +248,11 @@ namespace Barotrauma List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList(); - if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) + if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { - spawnWaypoints = WayPoint.WayPointList.FindAll(wp => + spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && - wp.Submarine == Level.Loaded.StartOutpost && + wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) @@ -262,9 +262,8 @@ namespace Barotrauma while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); - } + } } - if (spawnWaypoints == null || !spawnWaypoints.Any()) { spawnWaypoints = mainSubWaypoints; @@ -290,6 +289,16 @@ namespace Barotrauma else if (!character.Info.StartItemsGiven) { character.GiveJobItems(mainSubWaypoints[i]); + foreach (Item item in character.Inventory.AllItems) + { + //if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in + //we don't want that in this case, the crew's cards shouldn't be submarine-specific + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.SubmarineSpecificID = 0; + } + } } if (character.Info.HealthData != null) { @@ -298,6 +307,7 @@ namespace Barotrauma character.LoadTalents(); + character.GiveIdCardTags(mainSubWaypoints[i]); character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) @@ -410,20 +420,18 @@ namespace Barotrauma { List availableSpeakers = new List() { npc, player }; List dialogFlags = new List() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() }; + if (npc.HumanPrefab != null) + { + foreach (var tag in npc.HumanPrefab.GetTags()) + { + dialogFlags.Add(tag); + } + } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned") { - if (npc.TeamID == CharacterTeamType.None) - { - dialogFlags.Remove("OutpostNPC".ToIdentifier()); - dialogFlags.Add("Bandit".ToIdentifier()); - } - else if (npc.TeamID == CharacterTeamType.FriendlyNPC) - { - dialogFlags.Remove("OutpostNPC".ToIdentifier()); - dialogFlags.Add("Hostage".ToIdentifier()); - } + dialogFlags.Remove("OutpostNPC".ToIdentifier()); } else if (campaignMode.Map?.CurrentLocation?.Reputation != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 7f78f853d..004c007bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -8,19 +8,15 @@ namespace Barotrauma { internal partial class CampaignMetadata { - public CampaignMode Campaign { get; } - private readonly Dictionary data = new Dictionary(); - public CampaignMetadata(CampaignMode campaign) + public CampaignMetadata() { - Campaign = campaign; } - public CampaignMetadata(CampaignMode campaign, XElement element) + public void Load(XElement element) { - Campaign = campaign; - + data.Clear(); foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) @@ -59,10 +55,11 @@ namespace Barotrauma { DebugConsole.Log($"Set the value \"{identifier}\" to {value}"); + SteamAchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true); + if (!data.ContainsKey(identifier)) { data.Add(identifier, value); - SteamAchievementManager.OnCampaignMetadataSet(identifier, value); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 32dc12d72..b08da08a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,13 +1,16 @@ #nullable enable using Microsoft.Xna.Framework; -using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { public enum FactionAffiliation { - Affiliated, - Neutral + Positive, + Neutral, + Negative } class Faction @@ -25,21 +28,25 @@ namespace Barotrauma /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents /// /// - public FactionAffiliation GetPlayerAffiliationStatus() + public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction) { - float affiliation = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - if (character.Info is not { } info) { continue; } + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } - affiliation *= 1f + info.GetSavedStatValue(StatTypes.Affiliation, Prefab.Identifier); + bool isHighest = true; + foreach (Faction otherFaction in factions) + { + if (otherFaction == faction || otherFaction.Reputation.Value < faction.Reputation.Value) { continue; } + + isHighest = false; + break; } - return affiliation switch - { - >= 1f => FactionAffiliation.Affiliated, - _ => FactionAffiliation.Neutral - }; + return isHighest ? FactionAffiliation.Positive : FactionAffiliation.Negative; + } + + public override string ToString() + { + return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})"; } } @@ -52,6 +59,54 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public class HireableCharacter + { + public readonly Identifier NPCSetIdentifier; + public readonly Identifier NPCIdentifier; + public readonly float MinReputation; + + public HireableCharacter(ContentXElement element) + { + NPCSetIdentifier = element.GetAttributeIdentifier("from", element.GetAttributeIdentifier("npcsetidentifier", Identifier.Empty)); + NPCIdentifier = element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("npcidentifier", Identifier.Empty)); + MinReputation = element.GetAttributeFloat("minreputation", 0.0f); + } + } + + public ImmutableArray HireableCharacters; + + public class AutomaticMission + { + public readonly Identifier MissionTag; + public readonly LevelData.LevelType LevelType; + public readonly float MinReputation, MaxReputation; + public readonly float MinProbability, MaxProbability; + public readonly int MaxDistanceFromFactionOutpost; + public readonly bool DisallowBetweenOtherFactionOutposts; + + public AutomaticMission(ContentXElement element, string parentDebugName) + { + MissionTag = element.GetAttributeIdentifier("missiontag", Identifier.Empty); + LevelType = element.GetAttributeEnum("leveltype", LevelData.LevelType.LocationConnection); + MinReputation = element.GetAttributeFloat("minreputation", 0.0f); + MaxReputation = element.GetAttributeFloat("maxreputation", 0.0f); + if (MinReputation > MaxReputation) + { + DebugConsole.ThrowError($"Error in faction prefab \"{parentDebugName}\": MinReputation cannot be larger than MaxReputation."); + } + float probability = element.GetAttributeFloat("probability", 0.0f); + MinProbability = element.GetAttributeFloat("minprobability", probability); + MaxProbability = element.GetAttributeFloat("maxprobability", probability); + MaxDistanceFromFactionOutpost = element.GetAttributeInt(nameof(MaxDistanceFromFactionOutpost), int.MaxValue); + DisallowBetweenOtherFactionOutposts = element.GetAttributeBool(nameof(DisallowBetweenOtherFactionOutposts), false); + } + } + + public ImmutableArray AutomaticMissions; + + public bool StartOutpost { get; } + + public int MenuOrder { get; } /// @@ -69,38 +124,73 @@ namespace Barotrauma /// public int InitialReputation { get; } + public float ControlledOutpostPercentage { get; } + + public float SecondaryControlledOutpostPercentage { get; } + #if CLIENT public Sprite? Icon { get; private set; } + public Sprite? IconSmall { get; private set; } + public Sprite? BackgroundPortrait { get; private set; } +#endif public Color IconColor { get; } -#endif public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { MenuOrder = element.GetAttributeInt("menuorder", 0); + StartOutpost = element.GetAttributeBool("startoutpost", false); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); + ControlledOutpostPercentage = element.GetAttributeFloat("controlledoutpostpercentage", 0); + SecondaryControlledOutpostPercentage = element.GetAttributeFloat("secondarycontrolledoutpostpercentage", 0); Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); -#if CLIENT + + List hireableCharacters = new List(); + List automaticMissions = new List(); foreach (var subElement in element.Elements()) { - - if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) + var subElementId = subElement.NameAsIdentifier(); + if (subElementId == "icon") { IconColor = subElement.GetAttributeColor("color", Color.White); +#if CLIENT Icon = new Sprite(subElement); +#endif } - else if (subElement.Name.ToString().Equals("portrait", StringComparison.OrdinalIgnoreCase)) + else if (subElementId == "iconsmall") { +#if CLIENT + IconSmall = new Sprite(subElement); +#endif + } + else if (subElementId == "portrait") + { +#if CLIENT BackgroundPortrait = new Sprite(subElement); +#endif + } + else if (subElementId == "hireable") + { + hireableCharacters.Add(new HireableCharacter(subElement)); + } + else if (subElementId == "mission" || subElementId == "automaticmission") + { + automaticMissions.Add(new AutomaticMission(subElement, Identifier.ToString())); } } -#endif + HireableCharacters = hireableCharacters.ToImmutableArray(); + AutomaticMissions = automaticMissions.ToImmutableArray(); + } + + public override string ToString() + { + return $"{base.ToString()} ({Identifier})"; } public override void Dispose() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index ed89cf0c7..7205df632 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,17 +1,25 @@ using Microsoft.Xna.Framework; using System; -using System.Linq; namespace Barotrauma { class Reputation { public const float HostileThreshold = 0.2f; - public const float ReputationLossPerNPCDamage = 0.1f; - public const float ReputationLossPerStolenItemPrice = 0.01f; - public const float ReputationLossPerWallDamage = 0.1f; - public const float MinReputationLossPerStolenItem = 0.5f; - public const float MaxReputationLossPerStolenItem = 10.0f; + public const float ReputationLossPerNPCDamage = 0.025f; + public const float ReputationLossPerWallDamage = 0.025f; + public const float ReputationLossPerStolenItemPrice = 0.0025f; + public const float MinReputationLossPerStolenItem = 0.025f; + public const float MaxReputationLossPerStolenItem = 0.5f; + + /// + /// Maximum amount of reputation loss you can get from damaging outpost NPCs per round + /// + public const float MaxReputationLossFromNPCDamage = 10.0f; + /// + /// Maximum amount of reputation loss you can get from damaging outpost walls per round + /// + public const float MaxReputationLossFromWallDamage = 10.0f; public Identifier Identifier { get; } public int MinReputation { get; } @@ -19,6 +27,8 @@ namespace Barotrauma public int InitialReputation { get; } public CampaignMetadata Metadata { get; } + public float ReputationAtRoundStart { get; set; } + private readonly Identifier metaDataIdentifier; /// @@ -59,27 +69,47 @@ namespace Barotrauma Value = newReputation; } - public void AddReputation(float reputationChange) + public float GetReputationChangeMultiplier(float reputationChange) { if (reputationChange > 0f) { float reputationGainMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); + reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); + reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; } - reputationChange *= reputationGainMultiplier; + return reputationGainMultiplier; } else if (reputationChange < 0f) { float reputationLossMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationLossMultiplier += character.GetStatValue(StatTypes.ReputationLossMultiplier); + reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); + reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; } - reputationChange *= reputationLossMultiplier; + return reputationLossMultiplier; } - Value += reputationChange; + return 1.0f; + } + + public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue) + { + float currentValue = Value; + float currentReputationChange = currentValue - ReputationAtRoundStart; + if (Math.Abs(currentReputationChange) >= maxReputationChangePerRound && + Math.Sign(currentReputationChange) == Math.Sign(reputationChange)) + { + return; + } + float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange); + if (Math.Abs(newValue - ReputationAtRoundStart) > maxReputationChangePerRound && + Math.Sign(newValue - currentValue) == Math.Sign(newValue - ReputationAtRoundStart)) + { + newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange); + } + Value = newValue; } public readonly NamedEvent OnReputationValueChanged = new NamedEvent(); @@ -108,6 +138,7 @@ namespace Barotrauma metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier(); MinReputation = minReputation; MaxReputation = maxReputation; + ReputationAtRoundStart = initialReputation; InitialReputation = initialReputation; Faction = faction; Location = location; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index 795cb4680..98a2ebc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -69,7 +69,7 @@ namespace Barotrauma public Option RewardDistributionChanged; public Option BalanceChanged; - public WalletChangedData MergeInto(WalletChangedData other) + public readonly WalletChangedData MergeInto(WalletChangedData other) { other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); @@ -80,32 +80,20 @@ namespace Barotrauma static Option AddOptionalInt(Option a, Option b) { - return a switch - { - Some some1 => b switch - { - Some some2 => Option.Some(some1.Value + some2.Value), - None _ => Option.Some(some1.Value), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - None _ => b switch - { - Some some1 => Option.Some(some1.Value), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - _ => throw new ArgumentOutOfRangeException(nameof(a)) - }; + bool hasValue1 = a.TryUnwrap(out var value1); + bool hasValue2 = b.TryUnwrap(out var value2); + return hasValue1 + ? hasValue2 + ? Option.Some(value1 + value2) + : Option.Some(value1) + : hasValue2 + ? Option.Some(value2) + : Option.None; } static Option TurnToNoneIfZero(Option option) { - return option switch - { - Some s => s.Value == 0 ? Option.None() : option, - None _ => option, - _ => throw new ArgumentOutOfRangeException(nameof(option)) - }; + return option.Bind(i => i == 0 ? Option.None : Option.Some(i)); } } } @@ -223,12 +211,8 @@ namespace Barotrauma }; } - public string GetOwnerLogName() => Owner switch - { - Some { Value: var character } => character.Name, - None _ => "the bank", - _ => throw new ArgumentOutOfRangeException(nameof(Owner)) - }; + public string GetOwnerLogName() + => Owner.TryUnwrap(out var character) ? character.Name : "the bank"; partial void SettingsChanged(Option balanceChanged, Option rewardChanged); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index ddfcd55e0..81b26d81f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -23,8 +23,6 @@ namespace Barotrauma public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; - //duration of the cinematic + credits at the end of the campaign - protected const float EndCinematicDuration = 240.0f; //duration of the camera transition at the end of a round protected const float EndTransitionDuration = 5.0f; //there can be no events before this time has passed during the 1st campaign round @@ -44,9 +42,10 @@ namespace Barotrauma public UpgradeManager UpgradeManager; public MedicalClinic MedicalClinic; - public List Factions; + private List factions; + public IReadOnlyList Factions => factions; - public CampaignMetadata CampaignMetadata; + public readonly CampaignMetadata CampaignMetadata; protected XElement petsElement; @@ -84,9 +83,9 @@ namespace Barotrauma public bool CheatsEnabled; - public const float HullRepairCostPerDamage = 0.5f, ItemRepairCostPerRepairDuration = 1.0f; + public const float HullRepairCostPerDamage = 0.1f, ItemRepairCostPerRepairDuration = 1.0f; public const int ShuttleReplaceCost = 1000; - public const int MaxHullRepairCost = 2000, MaxItemRepairCost = 2000; + public const int MaxHullRepairCost = 600, MaxItemRepairCost = 2000; protected bool wasDocked; @@ -96,6 +95,8 @@ namespace Barotrauma public SubmarineInfo PendingSubmarineSwitch; public bool TransferItemsOnSubSwitch { get; set; } + public bool SwitchedSubsThisRound { get; private set; } + protected Map map; public Map Map { @@ -106,20 +107,24 @@ namespace Barotrauma { get { - if (Map.CurrentLocation != null) + //map can be null if we're in the process of loading the save + if (Map != null) { - foreach (Mission mission in map.CurrentLocation.SelectedMissions) + if (Map.CurrentLocation != null) { - if (mission.Locations[0] == mission.Locations[1] || - mission.Locations.Contains(Map.SelectedLocation)) + foreach (Mission mission in map.CurrentLocation.SelectedMissions) { - yield return mission; + if (mission.Locations[0] == mission.Locations[1] || + mission.Locations.Contains(Map.SelectedLocation)) + { + yield return mission; + } } } - } - foreach (Mission mission in extraMissions) - { - yield return mission; + foreach (Mission mission in extraMissions) + { + yield return mission; + } } } } @@ -141,10 +146,19 @@ namespace Barotrauma private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } - //allow managing if no-one with permissions is alive - return - GameMain.NetworkMember.ConnectedClients.Count == 1 || - GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + if (GameMain.NetworkMember.ConnectedClients.Count == 1) { return true; } + + if (GameMain.NetworkMember.GameStarted) + { + //allow managing if no-one with permissions is alive and in-game + return GameMain.NetworkMember.ConnectedClients.None(c => + c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && + (IsOwner(c) || c.HasPermission(permissions))); + } + else + { + return GameMain.NetworkMember.ConnectedClients.None(c => IsOwner(c) || c.HasPermission(permissions)); + } } protected CampaignMode(GameModePreset preset, CampaignSettings settings) @@ -157,26 +171,26 @@ namespace Barotrauma CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); + CampaignMetadata = new CampaignMetadata(); Identifier messageIdentifier = new Identifier("money"); #if CLIENT OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e => { - if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } + if (!e.ChangedData.BalanceChanged.TryUnwrap(out var changed)) { return; } if (changed == 0) { return; } bool isGain = changed > 0; Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; - switch (e.Owner) + if (e.Owner.TryUnwrap(out var owner)) { - case Some { Value: var owner}: - owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); - break; - case None _ when IsSinglePlayer: - Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); - break; + owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); + } + else if (IsSinglePlayer) + { + Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); } string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString(); @@ -239,6 +253,11 @@ namespace Barotrauma prevCampaignUIAutoOpenType = TransitionType.None; #endif + foreach (var faction in factions) + { + faction.Reputation.ReputationAtRoundStart = faction.Reputation.Value; + } + if (PurchasedHullRepairsInLatestSave) { foreach (Structure wall in Structure.WallList) @@ -272,6 +291,7 @@ namespace Barotrauma PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + SwitchedSubsThisRound = false; } public static int GetHullRepairCost() @@ -307,12 +327,12 @@ namespace Barotrauma return (int)Math.Min(totalRepairDuration * ItemRepairCostPerRepairDuration, MaxItemRepairCost); } - public void InitCampaignData() + public void InitFactions() { - Factions = new List(); + factions = new List(); foreach (FactionPrefab factionPrefab in FactionPrefab.Prefabs) { - Factions.Add(new Faction(CampaignMetadata, factionPrefab)); + factions.Add(new Faction(CampaignMetadata, factionPrefab)); } } @@ -358,10 +378,9 @@ namespace Barotrauma currentLocation.DeselectMission(mission); } } - if (levelData.HasBeaconStation && !levelData.IsBeaconActive) { - var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))).OrderBy(m => m.UintIdentifier); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconnoreward")).OrderBy(m => m.UintIdentifier); if (beaconMissionPrefabs.Any()) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); @@ -374,7 +393,7 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))).OrderBy(m => m.UintIdentifier); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier); if (!huntingGroundsMissionPrefabs.Any()) { DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); @@ -400,15 +419,108 @@ namespace Barotrauma weights[i] = weight; } var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); - if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) + if (!Missions.Any(m => m.Prefab.Tags.Contains("huntinggrounds"))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } } + foreach (Faction faction in factions.OrderBy(f => f.Prefab.MenuOrder)) + { + foreach (var automaticMission in faction.Prefab.AutomaticMissions) + { + if (faction.Reputation.Value < automaticMission.MinReputation || faction.Reputation.Value > automaticMission.MaxReputation) { continue; } + + if (automaticMission.DisallowBetweenOtherFactionOutposts && levelData.Type == LevelData.LevelType.LocationConnection) + { + if (Map.SelectedConnection.Locations.All(l => l.Faction != null && l.Faction != faction)) + { + continue; + } + } + if (automaticMission.MaxDistanceFromFactionOutpost < int.MaxValue) + { + if (!Map.LocationOrConnectionWithinDistance( + currentLocation, + automaticMission.MaxDistanceFromFactionOutpost, + loc => loc.Faction == faction)) + { + continue; + } + } + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed + TotalPassedLevels)); + if (levelData.Type != automaticMission.LevelType) { continue; } + float probability = + MathHelper.Lerp( + automaticMission.MinProbability, + automaticMission.MaxProbability, + MathUtils.InverseLerp(automaticMission.MinReputation, automaticMission.MaxReputation, faction.Reputation.Value)); + if (rand.NextDouble() < probability) + { + var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier); + if (missionPrefabs.Any()) + { + var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand); + if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate)) + { + continue; + } + if (automaticMission.LevelType == LevelData.LevelType.Outpost) + { + extraMissions.Add(missionPrefab.Instantiate(new Location[] { currentLocation, currentLocation }, Submarine.MainSub)); + } + else + { + extraMissions.Add(missionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); + } + } + } + } + } + } + if (levelData.Biome.IsEndBiome) + { + Identifier endMissionTag = Identifier.Empty; + if (levelData.Type == LevelData.LevelType.LocationConnection) + { + int locationIndex = map.EndLocations.IndexOf(map.SelectedLocation); + if (locationIndex > -1) + { + endMissionTag = ("endlevel_locationconnection_" + locationIndex).ToIdentifier(); + } + } + else + { + int locationIndex = map.EndLocations.IndexOf(map.CurrentLocation); + if (locationIndex > -1) + { + endMissionTag = ("endlevel_location_" + locationIndex).ToIdentifier(); + } + } + if (!endMissionTag.IsEmpty) + { + var endLevelMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(endMissionTag)).OrderBy(m => m.UintIdentifier); + if (endLevelMissionPrefabs.Any()) + { + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand); + if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type)) + { + if (levelData.Type == LevelData.LevelType.LocationConnection) + { + extraMissions.Add(endLevelMissionPrefab.Instantiate(map.SelectedConnection.Locations, Submarine.MainSub)); + } + else + { + extraMissions.Add(endLevelMissionPrefab.Instantiate(new Location[] { map.CurrentLocation, map.CurrentLocation }, Submarine.MainSub)); + } + } + } + } } } + public void LoadNewLevel() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -503,13 +615,6 @@ namespace Barotrauma { if (leavingSub.AtEndExit) { - if (Map.EndLocation != null && - map.SelectedLocation == Map.EndLocation && - Map.EndLocation.Connections.Any(c => c.LevelData == Level.Loaded.LevelData)) - { - nextLevel = map.StartLocation.LevelData; - return TransitionType.End; - } if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null) { nextLevel = Level.Loaded.EndLocation.LevelData; @@ -554,8 +659,32 @@ namespace Barotrauma } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData; - return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation; + int currentEndLocationIndex = map.EndLocations.IndexOf(map.CurrentLocation); + if (currentEndLocationIndex > -1) + { + if (currentEndLocationIndex == map.EndLocations.Count - 1) + { + //at the last end location, end of campaign + nextLevel = map.StartLocation?.LevelData; + return TransitionType.End; + } + else if (leavingSub.AtEndExit && currentEndLocationIndex < map.EndLocations.Count - 1) + { + //more end locations to go, progress to the next one + nextLevel = map.EndLocations[currentEndLocationIndex + 1]?.LevelData; + return TransitionType.ProgressToNextLocation; + } + else + { + nextLevel = null; + return TransitionType.None; + } + } + else + { + nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData; + return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation; + } } else { @@ -579,9 +708,11 @@ namespace Barotrauma //TODO: ignore players who don't have the permission to trigger a transition between levels? var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer)); + CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1; + //allow leaving if inside an outpost, and the submarine is either docked to it or close enough - Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers); - Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers); + Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam); + Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam); int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); @@ -595,11 +726,11 @@ namespace Barotrauma return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd; - static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { if (Level.Loaded.StartOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -609,26 +740,35 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtStartExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } } - static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { + if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any()) + { + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); + if (closestSub == null || !closestSub.AtEndExit) { return null; } + return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; + } //no "end" in outpost levels - if (Level.Loaded.Type == LevelData.LevelType.Outpost) { return null; } + if (Level.Loaded.Type == LevelData.LevelType.Outpost) + { + return null; + } if (Level.Loaded.EndOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -638,13 +778,13 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtEndExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -765,8 +905,8 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); - location.Reset(); + location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty); + location.Reset(this); } Map.ClearLocationHistory(); Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); @@ -779,6 +919,10 @@ namespace Barotrauma { location.TurnsInRadiation = 0; } + foreach (var faction in Factions) + { + faction.Reputation.SetReputation(faction.Prefab.InitialReputation); + } EndCampaignProjSpecific(); if (CampaignMetadata != null) @@ -800,11 +944,56 @@ namespace Barotrauma protected virtual void EndCampaignProjSpecific() { } + /// + /// Returns a random faction based on their ControlledOutpostPercentage + /// + /// If true, the method can return null if the sum of the factions ControlledOutpostPercentage is less than 100% + public Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty = true) + { + return GetRandomFaction(Factions, randSync, secondary: false, allowEmpty); + } + + /// + /// Returns a random faction based on their SecondaryControlledOutpostPercentage + /// + /// If true, the method can return null if the sum of the factions SecondaryControlledOutpostPercentage is less than 100% + public Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty = true) + { + return GetRandomFaction(Factions, randSync, secondary: true, allowEmpty); + } + + public static Faction GetRandomFaction(IEnumerable factions, Rand.RandSync randSync, bool secondary = false, bool allowEmpty = true) + { + return GetRandomFaction(factions, Rand.GetRNG(randSync), secondary, allowEmpty); + } + + public static Faction GetRandomFaction(IEnumerable factions, Random random, bool secondary = false, bool allowEmpty = true) + { + List factionsList = factions.OrderBy(f => f.Prefab.Identifier).ToList(); + List weights = factionsList.Select(f => secondary ? f.Prefab.SecondaryControlledOutpostPercentage : f.Prefab.ControlledOutpostPercentage).ToList(); + float percentageSum = weights.Sum(); + if (percentageSum < 100.0f && allowEmpty) + { + //chance of non-faction-specific outposts if percentage of controlled outposts is <100 + factionsList.Add(null); + weights.Add(100.0f - percentageSum); + } + return ToolBox.SelectWeightedRandom(factionsList, weights, random); + } + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) { if (characterInfo == null) { return false; } + if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) + { + if (GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + { + return false; + } + } if (!TryPurchase(client, characterInfo.Salary)) { return false; } characterInfo.IsNewHire = true; + characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); @@ -859,11 +1048,14 @@ namespace Barotrauma public void AssignNPCMenuInteraction(Character character, InteractionType interactionType) { character.CampaignInteractionType = interactionType; + if (character.CampaignInteractionType == InteractionType.Store && character.HumanPrefab is { Identifier: var merchantId }) { character.MerchantIdentifier = merchantId; + map.CurrentLocation?.GetStore(merchantId)?.SetMerchantFaction(character.Faction); } + character.DisableHealthWindow = interactionType != InteractionType.None && interactionType != InteractionType.Examine && @@ -975,11 +1167,36 @@ namespace Barotrauma if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; } if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; } if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; } - Location location = Map?.CurrentLocation; - if (location != null) + + if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction) { - location.Reputation.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); + faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage); } + else + { + Location location = Map?.CurrentLocation; + location?.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage); + } + } + + public Faction GetFaction(Identifier identifier) + { + return factions.Find(f => f.Prefab.Identifier == identifier); + } + + public float GetReputation(Identifier factionIdentifier) + { + var faction = + factionIdentifier == "location".ToIdentifier() ? + factions.Find(f => f == Map?.CurrentLocation?.Faction) : + factions.Find(f => f.Prefab.Identifier == factionIdentifier); + return faction?.Reputation?.Value ?? 0.0f; + } + + public FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier) + { + var faction = GetFaction(factionIdentifier); + return Faction.GetPlayerAffiliationStatus(faction); } public abstract void Save(XElement element); @@ -996,7 +1213,20 @@ namespace Barotrauma new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime), new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels)); } - + + protected void LoadEvents(XElement element) + { + TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0); + TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0); + } + + protected XElement SaveEvents() + { + return new XElement("events", + new XAttribute(nameof(EventManager.QueuedEventsForNextRound).ToLowerInvariant(), + string.Join(',', GameMain.GameSession.EventManager.QueuedEventsForNextRound))); + } + public void LogState() { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); @@ -1080,6 +1310,7 @@ namespace Barotrauma TransferItemsBetweenSubs(); } RefreshOwnedSubmarines(); + SwitchedSubsThisRound = true; PendingSubmarineSwitch = null; } @@ -1097,7 +1328,7 @@ namespace Barotrauma var itemsToTransfer = new List<(Item item, Item container)>(); if (PendingSubmarineSwitch != null) { - var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); // Remove items from the old sub foreach (Item item in Item.ItemList) { @@ -1132,15 +1363,29 @@ namespace Barotrauma { // Load the new sub var newSub = new Submarine(PendingSubmarineSwitch); - var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); - // Move the transferred items - List availableContainers = Item.ItemList - .Where(it => connectedSubs.Contains(it.Submarine) && it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed) - .Select(it => it.GetComponent()) - .Where(c => c != null) - .ToList(); + var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + WayPoint wp = WayPoint.WayPointList.FirstOrDefault(wp => wp.SpawnType == SpawnType.Cargo && connectedSubs.Contains(wp.Submarine)); + Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.FirstOrDefault(h => connectedSubs.Contains(h.Submarine) && !h.IsWetRoom); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); + return; + } + // First move the cargo containers, so that we can reuse them + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag("crate")); + foreach (var (item, oldContainer) in cargoContainers) + { + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); + item.CurrentHull = spawnHull; + item.Submarine = spawnHull.Submarine; + } + // Then move the other items + var cargoRooms = CargoManager.FindCargoRooms(newSub); + List availableContainers = CargoManager.FindReusableCargoContainers(connectedSubs).ToList(); foreach (var (item, oldContainer) in itemsToTransfer) { + if (cargoContainers.Contains((item, oldContainer))) { continue; } Item newContainer = null; item.Submarine = newSub; if (item.Container == null) @@ -1149,25 +1394,16 @@ namespace Barotrauma } if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) { - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub); - Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced(); - if (spawnHull == null) + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); + if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) { - DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); - return; + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } - if (spawnHull != null) + else if (cargoContainer.Item.Submarine is Submarine containerSub) { - var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); - if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) - { - Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); - item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); - } - } - else - { - DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + // Use the item's sub in case the sub consists of multiple linked subs. + item.Submarine = containerSub; } } string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 2377b386c..31aa8aa9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -11,11 +11,14 @@ namespace Barotrauma { public static CampaignSettings Empty => new CampaignSettings(element: null); +#if CLIENT + public static CampaignSettings CurrentSettings = new CampaignSettings(GameSettings.CurrentConfig.SavedCampaignSettings); +#endif public string Name => "CampaignSettings"; public const string LowerCaseSaveElementName = "campaignsettings"; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("Normal", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; [Serialize(true, IsPropertySaveable.Yes)] @@ -53,7 +56,6 @@ namespace Barotrauma return definition.GetInt(StartingBalanceAmount.ToIdentifier()); } return 8000; - } } @@ -65,7 +67,7 @@ namespace Barotrauma { return definition.GetFloat(Difficulty.ToIdentifier()); } - return 0; + return 0; } } @@ -82,7 +84,7 @@ namespace Barotrauma } public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 3; + public const int MaxMissionCountLimit = 10; public const int MinMissionCountLimit = 1; public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a84b1a7c5..2f5d725da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -53,7 +54,7 @@ namespace Barotrauma } } - private bool ValidateFlag(NetFlags flag) + private static bool ValidateFlag(NetFlags flag) { if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } #if DEBUG @@ -105,9 +106,8 @@ namespace Barotrauma #endif } CampaignID = currentCampaignID; - CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - InitCampaignData(); + InitFactions(); } public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) @@ -190,11 +190,20 @@ namespace Barotrauma //map already created, update it //if we're not downloading the initial save file (LastSaveID > 0), //show notifications about location type changes - map.LoadState(subElement, LastSaveID > 0); + map.LoadState(this, subElement, LastSaveID > 0); } break; case "metadata": - CampaignMetadata = new CampaignMetadata(this, subElement); + var prevReputations = Factions.ToDictionary(k => k, v => v.Reputation.Value); + CampaignMetadata.Load(subElement); + foreach (var faction in Factions) + { + if (!MathUtils.NearlyEqual(prevReputations[faction], faction.Reputation.Value)) + { + faction.Reputation.OnReputationValueChanged?.Invoke(faction.Reputation); + Reputation.OnAnyReputationValueChanged.Invoke(faction.Reputation); + } + } break; case "upgrademanager": case "pendingupgrades": @@ -214,6 +223,9 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case "eventmanager": + GameMain.GameSession.EventManager.Load(subElement); + break; case Wallet.LowerCaseSaveElementName: Bank = new Wallet(Option.None(), subElement); break; @@ -237,10 +249,8 @@ namespace Barotrauma }; } - CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); #if SERVER characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index d902e245a..e376e4e22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; +using Barotrauma.Extensions; namespace Barotrauma { @@ -75,7 +76,10 @@ namespace Barotrauma get { if (Map != null) { return Map.CurrentLocation; } - if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } + if (dummyLocations == null) + { + dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData); + } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } @@ -86,7 +90,10 @@ namespace Barotrauma get { if (Map != null) { return Map.SelectedLocation; } - if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } + if (dummyLocations == null) + { + dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData); + } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } @@ -248,13 +255,44 @@ namespace Barotrauma } } + public static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType = null) + { + MTRandom rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var forceParams = levelData?.ForceOutpostGenerationParams; + if (forceLocationType == null && + forceParams != null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains("Any".ToIdentifier())) + { + forceLocationType = + LocationType.Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand); + } + var dummyLocations = CreateDummyLocations(rand, forceLocationType); + List factions = new List(); + foreach (var factionPrefab in FactionPrefab.Prefabs) + { + factions.Add(new Faction(new CampaignMetadata(), factionPrefab)); + } + foreach (var location in dummyLocations) + { + if (location.Type.HasOutpost) + { + location.Faction = CampaignMode.GetRandomFaction(factions, rand, secondary: false); + location.SecondaryFaction = CampaignMode.GetRandomFaction(factions, rand, secondary: true); + } + } + return dummyLocations; + } + public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null) + { + return CreateDummyLocations(new MTRandom(ToolBox.StringToInt(seed)), forceLocationType); + } + + private static Location[] CreateDummyLocations(Random rand, LocationType? forceLocationType = null) { var dummyLocations = new Location[2]; - MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { - dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); + dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType); } return dummyLocations; } @@ -268,7 +306,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -286,11 +324,6 @@ namespace Barotrauma } } } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) - { - Campaign!.TryPurchase(client, cost); - } - GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; Campaign!.TransferItemsOnSubSwitch = transferItems; } @@ -298,10 +331,11 @@ namespace Barotrauma public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { if (Campaign is null) { return; } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, newSubmarine.Price)) { return; } + int price = newSubmarine.GetPrice(); + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { - GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); #if SERVER (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); @@ -395,6 +429,7 @@ namespace Barotrauma } } + GameMode!.AddExtraMissions(LevelData); foreach (Mission mission in GameMode!.Missions) { // setting level for missions that may involve difficulty-related submarine creation @@ -505,6 +540,8 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } + CharacterHUD.ClearBossProgressBars(); + RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); if (GameMode is not TutorialMode && GameMode is not TestGameMode) @@ -514,14 +551,15 @@ namespace Barotrauma { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); - if (missions.Count > 1) + var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage); + if (missionsToShow.Count() > 1) { string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name)); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false); } else { - var mission = missions.FirstOrDefault(); + var mission = missionsToShow.FirstOrDefault(); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false); } } @@ -568,7 +606,6 @@ namespace Barotrauma if (GameMode != null && Submarine != null) { missions.Clear(); - GameMode.AddExtraMissions(LevelData); missions.AddRange(GameMode.Missions); GameMode.Start(); foreach (Mission mission in missions) @@ -611,7 +648,7 @@ namespace Barotrauma } } - CreatureMetrics.Instance.RecentlyEncountered.Clear(); + CreatureMetrics.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundDuration = 0.0f; @@ -627,7 +664,16 @@ namespace Barotrauma return; } - if (level.StartOutpost != null) + var originalSubPos = Submarine.WorldPosition; + var spawnPoint = WayPoint.WayPointList.Find(wp => wp.SpawnType.HasFlag(SpawnType.Submarine) && wp.Submarine == level.StartOutpost); + if (spawnPoint != null) + { + //pre-determine spawnpoint, just use it directly + Submarine.SetPosition(spawnPoint.WorldPosition); + Submarine.NeutralizeBallast(); + Submarine.EnableMaintainPosition(); + } + else if (level.StartOutpost != null) { //start by placing the sub below the outpost Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders(); @@ -682,7 +728,7 @@ namespace Barotrauma else { Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f); - Submarine.NeutralizeBallast(); + Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } } @@ -691,6 +737,7 @@ namespace Barotrauma Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } + } else { @@ -841,7 +888,7 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) { GUI.ClearMessages(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); @@ -854,6 +901,7 @@ namespace Barotrauma TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); ObjectiveManager.ResetUI(); + CharacterHUD.ClearBossProgressBars(); #endif SteamAchievementManager.OnRoundEnded(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 43baec7c3..9e76c344f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -8,7 +8,7 @@ namespace Barotrauma public List AvailableCharacters { get; set; } public List PendingHires = new List(); - public const int MaxAvailableCharacters = 10; + public const int MaxAvailableCharacters = 6; public HireManager() { @@ -32,6 +32,24 @@ namespace Barotrauma var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); } + if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); } + if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); } + } + + private void GenerateFactionCharacters(FactionPrefab faction) + { + foreach (var character in faction.HireableCharacters) + { + HumanPrefab humanPrefab = NPCSet.Get(character.NPCSetIdentifier, character.NPCIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't create a hireable for the location: character prefab \"{character.NPCIdentifier}\" not found in the NPC set \"{character.NPCSetIdentifier}\"."); + continue; + } + var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); + characterInfo.MinReputationToHire = (faction.Identifier, character.MinReputation); + AvailableCharacters.Add(characterInfo); + } } public void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 2b40e6304..4d0a5186a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -14,6 +14,8 @@ namespace Barotrauma public enum NetworkHeader { REQUEST_AFFLICTIONS, + AFFLICTION_UPDATE, + UNSUBSCRIBE_ME, REQUEST_PENDING, ADD_PENDING, REMOVE_PENDING, @@ -295,6 +297,42 @@ namespace Barotrauma static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } + public static void OnAfflictionCountChanged(Character character) => + GameMain.GameSession?.Campaign?.MedicalClinic?.OnAfflictionCountChangedPrivate(character); + + private void OnAfflictionCountChangedPrivate(Character character) + { + if (character is not { CharacterHealth: { } health, Info: { } info }) { return; } + + ImmutableArray afflictions = GetAllAfflictions(health); + +#if CLIENT + if (GameMain.NetworkMember is null) + { + ui?.UpdateAfflictions(new NetCrewMember(info, afflictions)); + } + + ui?.UpdateCrewPanel(); +#elif SERVER + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + continue; + } + + if (sub.Target == info) + { + ServerSend(new NetCrewMember(info, afflictions), + header: NetworkHeader.AFFLICTION_UPDATE, + deliveryMethod: DeliveryMethod.Unreliable, + targetClient: sub.Subscriber); + } + } +#endif + } + public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; @@ -330,7 +368,7 @@ namespace Barotrauma new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, - new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } + new NetAffliction { Identifier = AfflictionPrefab.DamageType, Strength = 10, Price = 10 } }; #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs new file mode 100644 index 000000000..386a1836b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + class SlideshowPrefab : Prefab + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public class Slide + { + public readonly LocalizedString Text; + public readonly Sprite Portrait; + + public readonly float FadeInDelay, FadeInDuration, FadeOutDuration; + public readonly float TextFadeInDelay, TextFadeInDuration; + + public Slide(ContentXElement element) + { + string text = element.GetAttributeString(nameof(Text), string.Empty); + Text = TextManager.Get(text).Fallback(text); + + FadeInDelay = element.GetAttributeFloat(nameof(FadeInDelay), 0.0f); + FadeInDuration = element.GetAttributeFloat(nameof(FadeInDuration), 2.0f); + FadeOutDuration = element.GetAttributeFloat(nameof(FadeOutDuration), 2.0f); + TextFadeInDelay = element.GetAttributeFloat(nameof(TextFadeInDelay), 2.0f); + TextFadeInDuration = element.GetAttributeFloat(nameof(TextFadeInDuration), 3.0f); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "portrait": + Portrait = new Sprite(subElement, lazyLoad: true); + break; + } + } + } + } + + public readonly ImmutableArray Slides; + + public SlideshowPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + List slides = new List(); + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "slide": + slides.Add(new Slide(subElement)); + break; + } + } + Slides = slides.ToImmutableArray(); + } + + public override void Dispose() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index f627284a2..d644c8267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -105,15 +105,21 @@ namespace Barotrauma string slotString = subElement.GetAttributeString("slot", "None"); InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; - Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot, onSpawned: (Item item) => + + bool forceToSlot = subElement.GetAttributeBool("forcetoslot", false); + int amount = subElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) { - if (item != null && item.ParentInventory != this) + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: forceToSlot, slot: slot, onSpawned: (Item item) => { - string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - } - }); + if (item != null && item.ParentInventory != this) + { + string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + }); + } } } @@ -172,21 +178,6 @@ namespace Barotrauma (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } - public bool CanBeAutoMovedToCorrectSlots(Item item) - { - if (item == null) { return false; } - foreach (var allowedSlot in item.AllowedSlots) - { - InvSlotType slotsFree = InvSlotType.None; - for (int i = 0; i < slots.Length; i++) - { - if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } - } - if (allowedSlot == slotsFree) { return true; } - } - return false; - } - public override void RemoveItem(Item item) { RemoveItem(item, tryEquipFromSameStack: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 3ef615e8a..3c26d393f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -507,9 +507,9 @@ namespace Barotrauma.Items.Components wire.RemoveConnection(DockingTarget.item); powerConnection.TryAddLink(wire); - wire.Connect(powerConnection, false, false); + wire.TryConnect(powerConnection, addNode: false); recipient.TryAddLink(wire); - wire.Connect(recipient, false, false); + wire.TryConnect(recipient, addNode: false); //Flag connections to be updated Powered.ChangedConnections.Add(powerConnection); @@ -543,8 +543,8 @@ namespace Barotrauma.Items.Components System.Diagnostics.Debug.Assert(doorBody == null); doorBody = GameMain.World.CreateRectangle( - DockingTarget.Door.Body.width, - DockingTarget.Door.Body.height, + DockingTarget.Door.Body.Width, + DockingTarget.Door.Body.Height, 1.0f, position); doorBody.UserData = DockingTarget.Door; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 96d89ddc3..517c3d8f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using FarseerPhysics.Dynamics; #if CLIENT using Barotrauma.Lights; #endif @@ -13,6 +14,10 @@ namespace Barotrauma.Items.Components { partial class Door : Pickable, IDrawableComponent, IServerSerializable { + private static readonly HashSet doorList = new HashSet(); + + public static IReadOnlyCollection DoorList { get { return doorList; } } + private Gap linkedGap; private bool isOpen; @@ -89,6 +94,9 @@ namespace Barotrauma.Items.Components public PhysicsBody Body { get; private set; } + //the fixture that's part of the submarine's collider (= fixture that things outside the sub can collide with if the door is outside hulls) + public Fixture OutsideSubmarineFixture; + private float RepairThreshold { get { return item.GetComponent() == null ? 0.0f : item.MaxCondition; } @@ -162,9 +170,14 @@ namespace Barotrauma.Items.Components set { isOpen = value; - OpenState = (isOpen) ? 1.0f : 0.0f; + OpenState = isOpen ? 1.0f : 0.0f; } } + public bool IsClosed => !IsOpen; + + public bool IsFullyOpen => IsOpen && OpenState >= 1.0f; + + public bool IsFullyClosed => IsClosed && OpenState <= 0f; [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } @@ -226,6 +239,7 @@ namespace Barotrauma.Items.Components } IsActive = true; + doorList.Add(this); } public override void OnItemLoaded() @@ -394,11 +408,20 @@ namespace Barotrauma.Items.Components if (isClosing) { if (OpenState < 0.9f) { PushCharactersAway(); } + if (CheckSubmarinesInDoorWay()) + { + PredictedState = null; + isOpen = true; + } } else { bool wasEnabled = Body.Enabled; Body.Enabled = Impassable || openState < 1.0f; + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Body.Enabled ? SubmarineBody.CollidesWith : Category.None; + } if (wasEnabled && !Body.Enabled && IsHorizontal) { //when opening a hatch, force characters above it to refresh the floor position @@ -442,6 +465,10 @@ namespace Barotrauma.Items.Components } PushCharactersAway(); } + if (OutsideSubmarineFixture != null && Body.Enabled) + { + OutsideSubmarineFixture.CollidesWith = SubmarineBody.CollidesWith; + } #if CLIENT UpdateConvexHulls(); #endif @@ -462,10 +489,16 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } + + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Category.None; + } if (linkedGap != null) { linkedGap.Open = 1.0f; } + IsOpen = false; #if CLIENT if (convexHull != null) { convexHull.Enabled = false; } @@ -488,11 +521,8 @@ namespace Barotrauma.Items.Components { RefreshLinkedGap(); #if CLIENT - Vector2[] corners = GetConvexHullCorners(Rectangle.Empty); - - convexHull = new ConvexHull(corners, Color.Black, item); - if (Window != Rectangle.Empty) convexHull2 = new ConvexHull(corners, Color.Black, item); - + convexHull = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); + if (Window != Rectangle.Empty) { convexHull2 = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); } UpdateConvexHulls(); #endif } @@ -540,6 +570,36 @@ namespace Barotrauma.Items.Components convexHull?.Remove(); convexHull2?.Remove(); #endif + + doorList.Remove(this); + } + + private bool CheckSubmarinesInDoorWay() + { + if (linkedGap != null && linkedGap.IsRoomToRoom) { return false; } + + Rectangle doorRect = item.WorldRect; + if (IsHorizontal) + { + doorRect.Width = (int)(item.Rect.Width * (1.0f - openState)); + } + else + { + doorRect.Height = (int)(item.Rect.Height * (1.0f - openState)); + } + + foreach (Submarine sub in Submarine.Loaded) + { + if (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine)) { continue; } + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint(); + if (!Submarine.RectsOverlap(worldBorders, doorRect)) { continue; } + foreach (Hull hull in sub.GetHulls(alsoFromConnectedSubs: false)) + { + if (Submarine.RectsOverlap(hull.WorldRect, doorRect)) { return true; } + } + } + return false; } bool itemPosErrorShown; @@ -563,7 +623,6 @@ namespace Barotrauma.Items.Components Vector2 currSize = IsHorizontal ? new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) : new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState)); - Vector2 simSize = ConvertUnits.ToSimUnits(currSize); foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index ff4f8b21b..b039709e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -600,7 +600,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - //no further data needed, the event just triggers the discharge + msg.WriteUInt16(user?.ID ?? Entity.NullEntityID); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index c77aa4a7c..28264f432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -67,11 +67,18 @@ namespace Barotrauma.Items.Components [Serialize(true, IsPropertySaveable.Yes, "")] public bool CanSpawn { get; set; } = true; + [Editable, Serialize(false, IsPropertySaveable.Yes, "")] + public bool PreloadCharacter { get; set; } + private float spawnTimer; private float? spawnTimerGoal; private int spawnedAmount = 0; + private Character? preloadedCharacter; + + private bool preloadInitiated; + public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -103,12 +110,21 @@ namespace Barotrauma.Items.Components } } } - - base.OnItemLoaded(); } public override void Update(float deltaTime, Camera cam) { + if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated) + { + SpawnCharacter(Vector2.Zero, onSpawn: (Character c) => + { + preloadedCharacter = c; + c.DisabledByEvent = true; + }); + preloadInitiated = true; + return; + } + base.Update(deltaTime, cam); item.SendSignal(CanSpawn ? "1" : "0", "state_out"); @@ -269,10 +285,18 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrWhiteSpace(SpeciesName)) { - Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); - Identifier species = allSpecies.GetRandomUnsynced(); - Entity.Spawner?.AddCharacterToSpawnQueue(species, pos); - spawnedAmount++; + if (preloadedCharacter != null) + { + preloadedCharacter.DisabledByEvent = false; + preloadedCharacter.TeleportTo(pos); + preloadedCharacter = null; + spawnedAmount++; + } + else + { + SpawnCharacter(pos); + spawnedAmount++; + } } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { @@ -291,5 +315,15 @@ namespace Barotrauma.Items.Components } } } + + private void SpawnCharacter(Vector2 pos, Action? onSpawn = null) + { + if (!string.IsNullOrWhiteSpace(SpeciesName)) + { + Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier species = allSpecies.GetRandomUnsynced(); + Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index b59784eeb..6620a203c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -226,7 +226,7 @@ namespace Barotrauma.Items.Components Pusher = null; if (element.GetAttributeBool("blocksplayers", false)) { - Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, + Pusher = new PhysicsBody(item.body.Width, item.body.Height, item.body.Radius, item.body.Density, BodyType.Dynamic, Physics.CollisionItemBlocking, @@ -427,10 +427,11 @@ namespace Barotrauma.Items.Components return; } + //cannot hold and wear an item at the same time + //(unless the slot in which it's held and worn are equal - e.g. a suit with built-in tool or weapon on one hand) var wearable = item.GetComponent(); - if (wearable != null) + if (wearable != null && !wearable.AllowedSlots.SequenceEqual(allowedSlots)) { - //cannot hold and wear an item at the same time wearable.Unequip(character); } @@ -558,10 +559,16 @@ namespace Barotrauma.Items.Components public override bool OnPicked(Character picker) { +#if CLIENT if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { + if (!picker.Inventory.CanBeAutoMovedToCorrectSlots(item)) + { + picker.Inventory.FlashAllowedSlots(item, Color.Red); + } return false; } +#endif bool wasAttached = IsAttached; if (base.OnPicked(picker)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 4ad251c17..816ff3c9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -128,7 +128,7 @@ namespace Barotrauma.Items.Components if (body != null) { - trigger = new PhysicsBody(body.width, body.height, body.radius, + trigger = new PhysicsBody(body.Width, body.Height, body.Radius, body.Density, BodyType.Static, Physics.CollisionWall, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 548147bfc..da760edf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -445,7 +445,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 774bf603b..9751820c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -5,9 +5,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -98,6 +96,9 @@ namespace Barotrauma.Items.Components private set; } + private readonly IReadOnlySet suitableProjectiles; + + private enum ChargingState { Inactive, @@ -130,12 +131,11 @@ namespace Barotrauma.Items.Components // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; characterUsable = true; - + suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty()).ToHashSet(); if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) { DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement."); } - InitProjSpecific(element); } @@ -143,7 +143,8 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - ReloadTimer = Math.Min(reload, 1.0f); + //clamp above 1 to prevent rapid-firing by swapping weapons + ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer); IsActive = true; } @@ -259,7 +260,8 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; - float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f); + float spread = GetSpread(character) * Projectile.GetSpreadFromPool(); + var lastProjectile = LastProjectile; if (lastProjectile != projectile) { @@ -275,7 +277,7 @@ namespace Barotrauma.Items.Components { Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool()); } Item.RemoveContained(projectile.Item); } @@ -294,39 +296,41 @@ namespace Barotrauma.Items.Components public Projectile FindProjectile(bool triggerOnUseOnContainers = false) { - var containedItems = item.OwnInventory?.AllItemsMod; - if (containedItems == null) { return null; } - - foreach (Item item in containedItems) + foreach (ItemContainer container in item.GetComponents()) { - if (item == null) { continue; } - Projectile projectile = item.GetComponent(); - if (projectile != null) { return projectile; } - } - - //projectile not found, see if one of the contained items contains projectiles - foreach (Item it in containedItems) - { - if (it == null) { continue; } - var containedSubItems = it.OwnInventory?.AllItemsMod; - if (containedSubItems == null) { continue; } - foreach (Item subItem in containedSubItems) + foreach (Item containedItem in container.Inventory.AllItemsMod) { - if (subItem == null) { continue; } - Projectile projectile = subItem.GetComponent(); - //apply OnUse statuseffects to the container in case it has to react to it somehow - //(play a sound, spawn more projectiles, reduce condition...) - if (triggerOnUseOnContainers && subItem.Condition > 0.0f) + if (containedItem == null) { continue; } + Projectile projectile = containedItem.GetComponent(); + if (IsSuitableProjectile(projectile)) { return projectile; } + + //projectile not found, see if the contained item contains projectiles + var containedSubItems = containedItem.OwnInventory?.AllItemsMod; + if (containedSubItems == null) { continue; } + foreach (Item subItem in containedSubItems) { - subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f); - } - if (projectile != null) { return projectile; } + if (subItem == null) { continue; } + Projectile subProjectile = subItem.GetComponent(); + //apply OnUse statuseffects to the container in case it has to react to it somehow + //(play a sound, spawn more projectiles, reduce condition...) + if (triggerOnUseOnContainers && subItem.Condition > 0.0f) + { + subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f); + } + if (IsSuitableProjectile(subProjectile)) { return subProjectile; } + } } } - return null; } + private bool IsSuitableProjectile(Projectile projectile) + { + if (projectile?.Item == null) { return false; } + if (!suitableProjectiles.Any()) { return true; } + return suitableProjectiles.Any(s => projectile.Item.Prefab.Identifier == s || projectile.Item.HasTag(s)); + } + partial void LaunchProjSpecific(); } class AbilityRangedWeapon : AbilityObject, IAbilityItem diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 1451570d3..10143e307 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -100,6 +100,9 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")] + public bool IgnoreCharacters { get; set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } @@ -313,7 +316,11 @@ namespace Barotrauma.Items.Components private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + if (!IgnoreCharacters) + { + collisionCategories |= Physics.CollisionCharacter; + } //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them if (statusEffectLists != null) @@ -703,7 +710,7 @@ namespace Barotrauma.Items.Components private float repairTimer; private Gap previousGap; private readonly float repairTimeOut = 5; - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (!(objective.OperateTarget is Gap leak)) { @@ -901,17 +908,16 @@ namespace Barotrauma.Items.Components // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in currentTargets) { - if (!(target is Door door)) { continue; } + if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } - for (int i = 0; i < effect.propertyNames.Length; i++) + foreach (var propertyEffect in effect.PropertyEffects) { - Identifier propertyName = effect.propertyNames[i]; - if (propertyName != "stuck") { continue; } - if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } + if (propertyEffect.propertyName != "stuck") { continue; } + if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyEffect.propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); if (door.Stuck > 0) { - bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0; + bool isCutting = propertyEffect.value is float and < 0; var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, textTag: isCutting ? "progressbar.cutting" : "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 8b8ff6601..422152d7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -42,6 +42,8 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.5f; + public override bool Use(float deltaTime, Character character = null) { //actual throwing logic is handled in Update @@ -59,6 +61,7 @@ namespace Barotrauma.Items.Components base.Drop(dropper); throwState = ThrowState.None; throwAngle = ThrowAngleStart; + Item.ResetWaterDragCoefficient(); } public override void UpdateBroken(float deltaTime, Camera cam) @@ -97,6 +100,7 @@ namespace Barotrauma.Items.Components } item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; midAir = false; + Item.ResetWaterDragCoefficient(); } return; } @@ -188,6 +192,7 @@ namespace Barotrauma.Items.Components } item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); + item.WaterDragCoefficient = WaterDragCoefficient; item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); //disable platform collisions until the item comes back to rest again diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 9d8b2e08e..4f0491cde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -111,8 +111,9 @@ namespace Barotrauma.Items.Components private bool drawable = true; - [Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)] - public PropertyConditional.Comparison IsActiveConditionalComparison + #warning TODO: misnomer - should be IsActiveConditionalLogicalOperator + [Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)] + public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison { get; set; @@ -245,17 +246,10 @@ namespace Barotrauma.Items.Components [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } - /// /// Can be used by status effects or conditionals to the speed of the item /// - public float Speed - { - get - { - return item.Speed; - } - } + public float Speed => item.Speed; public readonly bool InheritStatusEffects; @@ -346,14 +340,8 @@ namespace Barotrauma.Items.Components { case "activeconditional": case "isactive": - IsActiveConditionals = IsActiveConditionals ?? new List(); - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - IsActiveConditionals.Add(new PropertyConditional(attribute)); - } - } + IsActiveConditionals ??= new List(); + IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "requireditem": case "requireditems": @@ -450,7 +438,7 @@ namespace Barotrauma.Items.Components public virtual void Drop(Character dropper) { } /// true if the operation was completed - public virtual bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { return false; } @@ -1033,7 +1021,7 @@ namespace Barotrauma.Items.Components prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null; if (prevRequiredItem != null) { - newRequiredItem.statusEffects = prevRequiredItem.statusEffects; + newRequiredItem.StatusEffects = prevRequiredItem.StatusEffects; newRequiredItem.Msg = prevRequiredItem.Msg; newRequiredItem.IsOptional = prevRequiredItem.IsOptional; newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 68c59aaee..a16f4572e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -12,20 +12,9 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { - class ActiveContainedItem - { - public readonly Item Item; - public readonly StatusEffect StatusEffect; - public readonly bool ExcludeBroken; - public readonly bool ExcludeFullCondition; - public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition) - { - Item = item; - StatusEffect = statusEffect; - ExcludeBroken = excludeBroken; - ExcludeFullCondition = excludeFullCondition; - } - } + readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); + + readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); class SlotRestrictions { @@ -63,7 +52,9 @@ namespace Barotrauma.Items.Components public readonly ItemInventory Inventory; private readonly List activeContainedItems = new List(); - + + private readonly List drawableContainedItems = new List(); + private List[] itemIds; //how many items can be contained @@ -351,8 +342,6 @@ namespace Barotrauma.Items.Components public void OnItemContained(Item containedItem) { - item.SetContainedItemPositions(); - int index = Inventory.FindIndex(containedItem); if (index >= 0 && index < slotRestrictions.Length) { @@ -362,7 +351,7 @@ namespace Barotrauma.Items.Components foreach (var containableItem in slotRestrictions[index].ContainableItems) { if (!containableItem.MatchesItem(containedItem)) { continue; } - foreach (StatusEffect effect in containableItem.statusEffects) + foreach (StatusEffect effect in containableItem.StatusEffects) { activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); } @@ -370,6 +359,14 @@ namespace Barotrauma.Items.Components } } + var relatedItem = FindContainableItem(containedItem); + drawableContainedItems.RemoveAll(d => d.Item == containedItem); + drawableContainedItems.Add(new DrawableContainedItem(containedItem, + Hide: relatedItem?.Hide ?? false, + ItemPos: relatedItem?.ItemPos, + Rotation: relatedItem?.Rotation ?? 0.0f)); + drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item))); + if (item.GetComponent() != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); @@ -384,6 +381,7 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } + item.SetContainedItemPositions(); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -396,6 +394,7 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); + drawableContainedItems.RemoveAll(i => i.Item == containedItem); //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); @@ -483,11 +482,11 @@ namespace Barotrauma.Items.Components { foreach (Item item in Inventory.AllItemsMod) { - item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter); - item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); + item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter); + item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter); item.GetComponent()?.Equip(ownerCharacter); - autoInjectCooldown = AutoInjectInterval; } + autoInjectCooldown = AutoInjectInterval; } } @@ -512,10 +511,18 @@ namespace Barotrauma.Items.Components if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); - if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects); + } + if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character) + { + effect.Apply(ActionType.OnContaining, deltaTime, item, character); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { @@ -759,54 +766,50 @@ namespace Barotrauma.Items.Components int i = 0; Vector2 currentItemPos = transformedItemPos; - foreach (Item contained in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(contained); - if (relatedItem != null) + if (contained.ItemPos.HasValue) { - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.Position; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.Position; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; } } - } + } - if (contained.body != null) + if (contained.Item.body != null) { try { Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); float rotation = itemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - rotation = MathHelper.ToRadians(relatedItem.Rotation); + rotation = MathHelper.ToRadians(contained.Rotation); } if (item.body != null) { @@ -817,29 +820,29 @@ namespace Barotrauma.Items.Components { rotation += -item.RotationRad; } - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); - contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); - contained.body.UpdateDrawPosition(); + contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); + contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation); + contained.Item.body.UpdateDrawPosition(); } catch (Exception e) { DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); - GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name, + GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name, GameAnalyticsManager.ErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); } - contained.body.Submarine = item.Submarine; + contained.Item.body.Submarine = item.Submarine; } - contained.Rect = + contained.Item.Rect = new Rectangle( - (int)(itemPos.X - contained.Rect.Width / 2.0f), - (int)(itemPos.Y + contained.Rect.Height / 2.0f), - contained.Rect.Width, contained.Rect.Height); + (int)(itemPos.X - contained.Item.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Item.Rect.Height / 2.0f), + contained.Item.Rect.Width, contained.Item.Rect.Height); - contained.Submarine = item.Submarine; - contained.CurrentHull = item.CurrentHull; - contained.SetContainedItemPositions(); + contained.Item.Submarine = item.Submarine; + contained.Item.CurrentHull = item.CurrentHull; + contained.Item.SetContainedItemPositions(); i++; if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index d6ae48ec5..e3d29d09b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -116,7 +116,7 @@ namespace Barotrauma.Items.Components { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); float currForce = force * voltageFactor; - float condition = item.Condition / item.MaxCondition; + float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); UpdateAITargets(noise); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0c41133e9..f8f3df86e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -267,7 +267,7 @@ namespace Barotrauma.Items.Components itemList.Enabled = true; if (amountInput != null) { - amountInput.Enabled = true; + amountInput.Enabled = amountTextMax.Enabled; } RefreshActivateButtonText(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 923623a3e..0401bda5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -235,7 +235,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { #if CLIENT if (GameMain.Client != null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index cdf2c9298..3ecd2752f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -671,7 +671,7 @@ namespace Barotrauma.Items.Components return picker != null; } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } character.AIController.SteeringManager.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 807c13d23..1beb6ee19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components private static readonly Dictionary> targetGroups = new Dictionary>(); - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 90922ec50..cc73b26fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -720,7 +720,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { character.AIController.SteeringManager.Reset(); if (objective.Override) @@ -813,7 +813,7 @@ namespace Barotrauma.Items.Components } } - sonar?.AIOperate(deltaTime, character, objective); + sonar?.CrewAIOperate(deltaTime, character, objective); if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("dialogicespirespottedsonar").Value, null, 0.0f, "icespirespottedsonar".ToIdentifier(), 60.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 4c90945ba..45d2af075 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 23c9487a8..50dce4efd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -6,6 +6,7 @@ using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Voronoi2; @@ -13,6 +14,20 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent, IServerSerializable { + const int SpreadCounterWrapAround = 256; + + private static readonly ImmutableArray spreadPool; + static Projectile() + { + MTRandom random = new MTRandom(0); + spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); + } + + public static float GetSpreadFromPool() + { + return spreadPool[SpreadCounter]; + } + struct HitscanResult { public Fixture Fixture; @@ -41,10 +56,14 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.1f; + private readonly Queue impactQueue = new Queue(); private bool removePending; + public static byte SpreadCounter { get; private set; } + //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -192,7 +211,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] + [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")] public bool StaticSpread { get; @@ -280,6 +299,8 @@ namespace Barotrauma.Items.Components return; } + SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround); + InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); @@ -292,13 +313,13 @@ namespace Barotrauma.Items.Components switch (item.body.BodyShape) { case PhysicsBody.Shape.Circle: - Attack.DamageRange = item.body.radius; + Attack.DamageRange = item.body.Radius; break; case PhysicsBody.Shape.Capsule: - Attack.DamageRange = item.body.height / 2 + item.body.radius; + Attack.DamageRange = item.body.Height / 2 + item.body.Radius; break; case PhysicsBody.Shape.Rectangle: - Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); + Attack.DamageRange = new Vector2(item.body.Width / 2.0f, item.body.Height / 2.0f).Length(); break; } Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); @@ -358,8 +379,8 @@ namespace Barotrauma.Items.Components if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { #if SERVER - launchRot = rotation; - Item.CreateServerEvent(this, new EventData(launch: true)); + launchRot = rotation; + Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1))); #endif } } @@ -368,23 +389,23 @@ namespace Barotrauma.Items.Components { if (character != null && !characterUsable) { return false; } if (item.body == null) { return false; } + //can't launch if already launched + if (StickTarget != null || IsActive) { return false; } + float initialRotation = item.body.Rotation; for (int i = 0; i < HitScanCount; i++) { float launchAngle; - + if (StaticSpread) { - float staticSpread = Spread / (HitScanCount - 1); - // because the position of the item changes as hitscan are fired, we will set an - // initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired - float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f; - launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset); + launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread; } else { - launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f)); + launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool()); } + SpreadCounter++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) @@ -401,8 +422,7 @@ namespace Barotrauma.Items.Components { item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); - DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); - System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); + DoLaunch(launchDir * modifiedLaunchImpulse); } } User = character; @@ -423,15 +443,26 @@ namespace Barotrauma.Items.Components } item.Drop(null, createNetworkEvent: false); + Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; item.body.Enabled = true; - item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); + if (item.body.BodyType == BodyType.Kinematic) + { + item.body.LinearVelocity = impulse; + } + else + { + impulse *= item.body.Mass; + item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); + } item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; + EnableProjectileCollisions(); + IsActive = true; if (stickJoint == null) { return; } @@ -447,6 +478,7 @@ namespace Barotrauma.Items.Components Vector2 simPositon = item.SimPosition; Vector2 rayStartWorld = item.WorldPosition; item.Drop(null); + Item.WaterDragCoefficient = WaterDragCoefficient; item.body.Enabled = true; //set the velocity of the body because the OnProjectileCollision method @@ -505,6 +537,7 @@ namespace Barotrauma.Items.Components { var h = hits[i]; item.SetTransform(h.Point, rotation); + item.UpdateTransform(); if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { hitCount++; @@ -560,6 +593,8 @@ namespace Barotrauma.Items.Components return true; } if (fixture.Body.UserData is VineTile) { return true; } + if (fixture.CollidesWith == Category.None) { return true; } + if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub @@ -611,6 +646,7 @@ namespace Barotrauma.Items.Components return -1; } if (fixture.Body.UserData is VineTile) { return -1; } + if (fixture.CollidesWith == Category.None) { return -1; } if (fixture.Body.UserData is Item item) { if (item.Condition <= 0) { return -1; } @@ -669,6 +705,7 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { + Item.ResetWaterDragCoefficient(); if (dropper != null) { DisableProjectileCollisions(); @@ -755,6 +792,7 @@ namespace Barotrauma.Items.Components { if (User != null && User.Removed) { User = null; return false; } if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; } + if (originalCollisionCategories == Category.None && originalCollisionTargets == Category.None) { return false; } //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -840,8 +878,24 @@ namespace Barotrauma.Items.Components } if (target.Body.UserData is Submarine sub) { - Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? - contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); + //hit an item in a different sub -> no need to ignore, we can process the impact with this info + //(if it wasn't, we'll move the projectile to that sub's coordinate space and let it hit what it hits there) + if (Launcher?.Submarine != sub && target.UserData is Item) + { + return false; + } + + Vector2 normalizedVel; + Vector2 dir; + if (item.body.LinearVelocity.LengthSquared() < 0.001f) + { + normalizedVel = Vector2.Zero; + dir = contact.Manifold.LocalNormal; + } + else + { + normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity); + } //do a raycast in the sub's coordinate space to see if it hit a structure var wallBody = Submarine.PickBody( @@ -850,7 +904,7 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionWall); if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) + Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) @@ -886,7 +940,7 @@ namespace Barotrauma.Items.Components AttackResult attackResult = new AttackResult(); Character character = null; - if (target.Body.UserData is Submarine submarine) + if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item) { item.Move(-submarine.Position); item.Submarine = submarine; @@ -911,9 +965,11 @@ namespace Barotrauma.Items.Components if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); } if (limb.character != null) { character = limb.character; } } - else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem) + else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem) { if (targetItem.Removed) { return false; } + //hit the external collider of an item (turret?) of the same sub -> ignore + if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; } if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent() != null) && targetItem.Condition > 0) { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); @@ -925,7 +981,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } @@ -1091,10 +1147,14 @@ namespace Barotrauma.Items.Components private void EnableProjectileCollisions() { - item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; - if (!IgnoreProjectilesWhileActive) + if (item.body.CollisionCategories != Category.None) { + item.body.CollisionCategories = Physics.CollisionProjectile; + item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + } + if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive) + { + if (item.body.CollisionCategories == Category.None) { item.body.CollisionCategories = Physics.CollisionCharacter; } item.body.CollidesWith |= Physics.CollisionProjectile; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e5604f449..66e6e93e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,12 +1,11 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -17,6 +16,9 @@ namespace Barotrauma.Items.Components private float deteriorationTimer; private float deteriorateAlwaysResetTimer; + private int updateDeteriorationCounter; + private const int UpdateDeteriorationInterval = 10; + private int prevSentConditionValue; private string conditionSignal; @@ -232,6 +234,7 @@ namespace Barotrauma.Items.Components public float RepairDegreeOfSuccess(Character character, List skills) { if (skills.Count == 0) { return 1.0f; } + if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; @@ -241,6 +244,7 @@ namespace Barotrauma.Items.Components public void RepairBoost(bool qteSuccess) { + if (CurrentFixer == null) { return; } if (qteSuccess) { item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); @@ -404,26 +408,11 @@ namespace Barotrauma.Items.Components #endif } } - if (!ShouldDeteriorate()) { return; } - if (item.Condition > 0.0f) + updateDeteriorationCounter++; + if (updateDeteriorationCounter >= UpdateDeteriorationInterval) { - if (deteriorationTimer > 0.0f) - { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) - { - deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); -#if SERVER - if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } -#endif - } - return; - } - - if (item.ConditionPercentage > MinDeteriorationCondition) - { - float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); - item.Condition -= deteriorationSpeed * deltaTime; - } + UpdateDeterioration(deltaTime * UpdateDeteriorationInterval); + updateDeteriorationCounter = 0; } return; } @@ -559,6 +548,30 @@ namespace Barotrauma.Items.Components } } + private void UpdateDeterioration(float deltaTime) + { + if (item.Condition <= 0.0f) { return; } + if (!ShouldDeteriorate()) { return; } + + if (deteriorationTimer > 0.0f) + { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); +#if SERVER + if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } +#endif + } + return; + } + + if (item.ConditionPercentage > MinDeteriorationCondition) + { + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; + } + } + private float GetMaxRepairConditionMultiplier(Character character) { if (character == null) { return 1.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 224d1882f..b73b7cbf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components public readonly List Effects; - public readonly List LoadedWireIds; + public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires; //The grid the connection is a part of public GridInfo Grid; @@ -151,16 +151,20 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - LoadedWireIds = new List(); + LoadedWires = new List<(ushort wireId, int? connectionIndex)>(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "link": int id = subElement.GetAttributeInt("w", 0); + int? i = null; + if (subElement.GetAttribute("i") != null) + { + i = subElement.GetAttributeInt("i", 0); + } if (id < 0) { id = 0; } - if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); } - + if (LoadedWires.Count < MaxWires) { LoadedWires.Add((idRemap.GetOffsetId(id), i)); } break; case "statuseffect": Effects ??= new List(); @@ -351,22 +355,29 @@ namespace Barotrauma.Items.Components public void InitializeFromLoaded() { - if (LoadedWireIds.Count == 0) { return; } + if (LoadedWires.Count == 0) { return; } - for (int i = 0; i < LoadedWireIds.Count; i++) + foreach ((ushort wireId, int? connectionIndex) in LoadedWires) { - if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; } + if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; } var wire = wireItem.GetComponent(); if (wire != null && TryAddLink(wire)) { - if (wire.Item.body != null) wire.Item.body.Enabled = false; - wire.Connect(this, false, false); + if (wire.Item.body != null) { wire.Item.body.Enabled = false; } + if (connectionIndex.HasValue) + { + wire.Connect(this, connectionIndex.Value, addNode: false, sendNetworkEvent: false); + } + else + { + wire.TryConnect(this, addNode: false, sendNetworkEvent: false); + } wire.FixNodeEnds(); recipientsDirty = true; } } - LoadedWireIds.Clear(); + LoadedWires.Clear(); } @@ -377,7 +388,8 @@ namespace Barotrauma.Items.Components foreach (var wire in wires.OrderBy(w => w.Item.ID)) { newElement.Add(new XElement("link", - new XAttribute("w", wire.Item.ID.ToString()))); + new XAttribute("w", wire.Item.ID.ToString()), + new XAttribute("i", wire.Connections[0] == this ? 0 : 1))); } parentElement.Add(newElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 3954c06ed..7c9fd3a1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -295,8 +295,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++) { - Connections[i].LoadedWireIds.Clear(); - Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds); + Connections[i].LoadedWires.Clear(); + Connections[i].LoadedWires.AddRange(loadedConnections[i].LoadedWires); } disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index dc2ae516c..eb76f17ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -243,14 +243,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < labels.Length; i++) { labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label; - if (Screen.Selected != GameMain.SubEditorScreen) - { - customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value; - } - else - { - customInterfaceElementList[i].Label = labels[i]; - } + customInterfaceElementList[i].Label = labels[i]; } UpdateLabelsProjSpecific(); } @@ -304,9 +297,12 @@ namespace Barotrauma.Items.Components } #if SERVER //make sure the clients know about the states of the checkboxes and text fields - if (item.Submarine == null || !item.Submarine.Loading) + if (customInterfaceElementList.Any()) { - item.CreateServerEvent(this); + if (item.Submarine == null || !item.Submarine.Loading) + { + item.CreateServerEvent(this); + } } #endif } @@ -326,7 +322,7 @@ namespace Barotrauma.Items.Components } foreach (StatusEffect effect in btnElement.StatusEffects) { - item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f); + item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index da887ef93..097d23817 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; using Barotrauma.Networking; using Barotrauma.Extensions; #if CLIENT @@ -13,6 +12,9 @@ namespace Barotrauma.Items.Components partial class LightComponent : Powered, IServerSerializable, IDrawableComponent { private Color lightColor; + /// + /// The current brightness of the light source, affected by powerconsumption/voltage + /// private float lightBrightness; private float blinkFrequency; private float pulseFrequency, pulseAmount; @@ -94,7 +96,7 @@ namespace Barotrauma.Items.Components if (isOn == value && IsActive == value) { return; } IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); OnStateChanged(); } } @@ -174,7 +176,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (Light != null) { - Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent; + Light.Color = IsOn ? lightColor.Multiply(lightColorMultiplier) : Color.Transparent; } #endif } @@ -187,7 +189,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] public bool AlphaBlend { get; @@ -214,7 +216,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); } } @@ -228,7 +230,7 @@ namespace Barotrauma.Items.Components Position = item.Position, CastShadows = castShadows, IsBackground = drawBehindSubs, - SpriteScale = Vector2.One * item.Scale, + SpriteScale = Vector2.One * item.Scale * LightSpriteScale, Range = range }; Light.LightSourceParams.Flicker = flicker; @@ -245,7 +247,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - SetLightSourceState(IsActive); + SetLightSourceState(IsActive, lightBrightness); turret = item.GetComponent(); #if CLIENT Drawable = AlphaBlend && Light.LightSprite != null; @@ -258,6 +260,12 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { +#if CLIENT + if (item.HiddenInGame) + { + Light.Enabled = false; + } +#endif CheckIfNeedsUpdate(); } @@ -273,7 +281,8 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - SetLightSourceState(true); + lightBrightness = 1.0f; + SetLightSourceState(true, lightBrightness); SetLightSourceTransformProjSpecific(); base.IsActive = false; isOn = true; @@ -300,18 +309,21 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null && item.GetRootInventoryOwner() is not Character) + var ownerCharacter = item.GetRootInventoryOwner() as Character; + if ((item.Container != null && ownerCharacter == null) || + (ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f)) { - SetLightSourceState(false); + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); return; } - SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) { - SetLightSourceState(false); + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); return; } @@ -338,7 +350,7 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { - SetLightSourceState(false); + SetLightSourceState(false, 0.0f); } public override bool Use(float deltaTime, Character character = null) @@ -370,7 +382,7 @@ namespace Barotrauma.Items.Components { LightColor = XMLExtensions.ParseColor(signal.value, false); #if CLIENT - SetLightSourceState(Light.Enabled); + SetLightSourceState(Light.Enabled, lightColorMultiplier); #endif prevColorSignal = signal.value; } @@ -388,7 +400,7 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } - partial void SetLightSourceState(bool enabled, float? brightness = null); + partial void SetLightSourceState(bool enabled, float brightness); public void SetLightSourceTransform() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6ebfe3fc8..93527069d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 1d6188ebd..4f09c6764 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private const int MaxMessages = 60; - private List messageHistory = new List(MaxMessages); + private readonly List messageHistory = new List(MaxMessages); public LocalizedString DisplayedWelcomeMessage { @@ -67,6 +67,12 @@ namespace Barotrauma.Items.Components [Editable, Serialize(false, IsPropertySaveable.Yes, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] public bool UseMonospaceFont { get; set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AutoHideScrollbar { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool WelcomeMessageDisplayed { get; set; } + private Color textColor = Color.LimeGreen; [Editable, Serialize("50,205,50,255", IsPropertySaveable.Yes, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] @@ -85,6 +91,15 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize("> ", IsPropertySaveable.Yes)] + public string LineStartSymbol { get; set; } + + [Editable, Serialize(false, IsPropertySaveable.No)] + public bool Readonly { get; set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool AutoScrollToBottom { get; set; } + private string OutputValue { get; set; } private string prevColorSignal; @@ -143,14 +158,14 @@ namespace Barotrauma.Items.Components #endif base.OnItemLoaded(); - if (!DisplayedWelcomeMessage.IsNullOrEmpty()) + if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed) { ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); DisplayedWelcomeMessage = ""; - //remove welcome message if a game session is running so it doesn't reappear on successive rounds + //disable welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) { - welcomeMessage = null; + WelcomeMessageDisplayed = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 09dfe9080..09563de0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -162,14 +162,50 @@ namespace Barotrauma.Items.Components SetConnectedDirty(); } - public bool Connect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false) + /// + /// Tries to add the given connection to this wire. Note that this only affects the wire - + /// adding the wire to the connection is done in + /// + + public bool TryConnect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false) + { + if (connections[0] == null) + { + return Connect(newConnection, 0, addNode, sendNetworkEvent); + } + else if (connections[1] == null) + { + return Connect(newConnection, 1, addNode, sendNetworkEvent); + } + return false; + } + + + /// + /// Tries to add the given connection to this wire. Note that this only affects the wire - + /// adding the wire to the connection is done in + /// + /// Which end of the wire to add the connection to? 0 or 1. + /// Normally doesn't make a difference, but matters if we're copying/loading a wire, + /// in which case the 1st node should be located at the same item as the 1st connection. + /// + public bool Connect(Connection newConnection, int connectionIndex, bool addNode = true, bool sendNetworkEvent = false) { for (int i = 0; i < 2; i++) { if (connections[i] == newConnection) { return false; } } - if (!connections.Any(c => c == null)) { return false; } + if (connectionIndex < 0 || connectionIndex > 1) + { + DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: {connectionIndex} is not a valid index."); + return false; + } + if (connections[connectionIndex] != null) + { + DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: a wire is already connected to the index {connectionIndex}."); + return false; + } for (int i = 0; i < 2; i++) { @@ -183,70 +219,12 @@ namespace Barotrauma.Items.Components newConnection.ConnectionPanel.DisconnectedWires.Remove(this); - for (int i = 0; i < 2; i++) + connections[connectionIndex] = newConnection; + FixNodeEnds(); + + if (addNode) { - if (connections[i] != null) { continue; } - - connections[i] = newConnection; - FixNodeEnds(); - - if (!addNode) { break; } - - Submarine refSub = newConnection.Item.Submarine; - if (refSub == null) - { - Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); - if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) - { - connections[i] = null; - continue; - } - refSub = attachTarget?.Submarine; - } - - Vector2 nodePos = refSub == null ? - newConnection.Item.Position : - newConnection.Item.Position - refSub.HiddenSubPosition; - - if (nodes.Count > 0 && nodes[0] == nodePos) { break; } - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; } - - //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) - int newNodeIndex = 0; - if (nodes.Count > 1) - { - if (connections[0] != null && connections[0] != newConnection) - { - if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < - Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) - { - newNodeIndex = nodes.Count; - } - } - else if (connections[1] != null && connections[1] != newConnection) - { - if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < - Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) - { - newNodeIndex = nodes.Count; - } - } - else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) - { - newNodeIndex = nodes.Count; - } - } - - if (newNodeIndex == 0 && nodes.Count > 1) - { - nodes.Insert(0, nodePos); - } - else - { - nodes.Add(nodePos); - } - - break; + AddNode(newConnection, connectionIndex); } SetConnectedDirty(); @@ -258,7 +236,7 @@ namespace Barotrauma.Items.Components if (ic == this) { continue; } ic.Drop(null); } - if (item.Container != null) { item.Container.RemoveContained(this.item); } + item.Container?.RemoveContained(item); if (item.body != null) { item.body.Enabled = false; } IsActive = false; @@ -286,6 +264,63 @@ namespace Barotrauma.Items.Components return true; } + private void AddNode(Connection newConnection, int selectedIndex) + { + Submarine refSub = newConnection.Item.Submarine; + if (refSub == null) + { + Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); + if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) + { + connections[selectedIndex] = null; + return; + } + refSub = attachTarget?.Submarine; + } + + Vector2 nodePos = refSub == null ? + newConnection.Item.Position : + newConnection.Item.Position - refSub.HiddenSubPosition; + + if (nodes.Count > 0 && nodes[0] == nodePos) { return; } + if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; } + + //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) + int newNodeIndex = 0; + if (nodes.Count > 1) + { + if (connections[0] != null && connections[0] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) + { + newNodeIndex = nodes.Count; + } + } + else if (connections[1] != null && connections[1] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) + { + newNodeIndex = nodes.Count; + } + } + else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) + { + newNodeIndex = nodes.Count; + } + } + + if (newNodeIndex == 0 && nodes.Count > 1) + { + nodes.Insert(0, nodePos); + } + else + { + nodes.Add(nodePos); + } + } + public override void Equip(Character character) { if (shouldClearConnections) { ClearConnections(character); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 012548826..85e995e5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -67,7 +67,13 @@ namespace Barotrauma.Items.Components { return GameMain.GameSession?.RoundDuration ?? 0.0f; } - } + } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool ApplyEffectsToCharactersInsideSub { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool MoveOutsideSub { get; set; } private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); @@ -124,7 +130,7 @@ namespace Barotrauma.Items.Components PhysicsBody.FarseerBody.SetIsSensor(true); PhysicsBody.FarseerBody.OnCollision += OnCollision; PhysicsBody.FarseerBody.OnSeparation += OnSeparation; - RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius); + RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius); } public override void OnMapLoaded() @@ -137,7 +143,7 @@ namespace Barotrauma.Items.Components private bool OnCollision(Fixture sender, Fixture other, Contact contact) { if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } - if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; } + if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; } triggerers.Add(entity); return true; } @@ -162,6 +168,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (item.Submarine != null && MoveOutsideSub) + { + item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation); + item.CurrentHull = null; + item.Submarine = null; + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + LevelTrigger.RemoveInActiveTriggerers(PhysicsBody, triggerers); if (triggerOnce) @@ -201,6 +216,13 @@ namespace Barotrauma.Items.Components else if (triggerer is Submarine submarine) { LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); + foreach (Character c2 in Character.CharacterList) + { + if (c2.Submarine == submarine) + { + LevelTrigger.ApplyAttacks(attacks, c2, item.WorldPosition, deltaTime); + } + } } if (Math.Abs(Force) < 0.01f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 221ce222a..263bb9f9a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -59,8 +59,9 @@ namespace Barotrauma.Items.Components private float aiTargetingGraceTimer; private float aiFindTargetTimer; - private Character currentTarget; - const float aiFindTargetInterval = 5.0f; + private ISpatialEntity currentTarget; + private const float CrewAiFindTargetMaxInterval = 3.0f; + private const float CrewAIFindTargetMinInverval = 0.2f; private int currentLoaderIndex; @@ -73,6 +74,8 @@ namespace Barotrauma.Items.Components private List lightComponents; + private readonly bool isSlowTurret; + public float Rotation { get { return rotation; } @@ -317,6 +320,42 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), Editable] + public bool AutoOperate { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), Editable] + public float RandomAimAmount { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), Editable] + public float RandomAimMinTime { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), Editable] + public float RandomAimMaxTime { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), Editable] + public bool RandomMovement { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), Editable] + public bool AimDelay { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), Editable] + public bool TargetCharacters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), Editable] + public bool TargetMonsters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), Editable] + public bool TargetHumans { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), Editable] + public bool TargetSubmarines { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), Editable] + public bool TargetItems { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable] + public Identifier FriendlyTag { get; private set; } + public Turret(Item item, ContentXElement element) : base(item, element) { @@ -346,6 +385,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = false; + isSlowTurret = item.HasTag("slowturret"); InitProjSpecific(element); } @@ -560,6 +600,11 @@ namespace Barotrauma.Items.Components } UpdateLightComponents(); + + if (AutoOperate) + { + UpdateAutoOperate(deltaTime); + } } public void UpdateLightComponents() @@ -658,13 +703,20 @@ namespace Barotrauma.Items.Components loaderBroken = true; continue; } - ItemContainer projectileContainer = linkedItem.GetComponent(); + if (tryUseProjectileContainer(linkedItem)) { break; } + } + tryUseProjectileContainer(item); + + bool tryUseProjectileContainer(Item containerItem) + { + ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - linkedItem.Use(deltaTime, null); + containerItem.Use(deltaTime, null); projectiles = GetLoadedProjectiles(); - if (projectiles.Any()) { break; } + if (projectiles.Any()) { return true; } } + return false; } } if (projectiles.Count == 0 && !LaunchWithoutProjectile) @@ -833,9 +885,21 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); - projectile.SetTransform( - ConvertUnits.ToSimUnits(GetRelativeFiringPosition()), - -(launchRotation ?? rotation) + spread); + + Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition()); + + //check if there's some other sub between the turret's origin and the launch pos, + //and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub + Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true, + customPredicate: (Fixture f) => + { + return f.Body.UserData is not Submarine sub || sub != item.Submarine; + }); + if (pickedBody != null) + { + launchPos = Submarine.LastPickedPosition; + } + projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -895,17 +959,21 @@ namespace Barotrauma.Items.Components } private float waitTimer; - private float disorderTimer; + private float randomAimTimer; private float prevTargetRotation; private float updateTimer; private bool updatePending; - public void ThalamusOperate(WreckAI ai, float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) - { - if (ai == null) { return; } + public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default) + { IsActive = true; + if (friendlyTag.IsEmpty) + { + friendlyTag = FriendlyTag; + } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -924,7 +992,7 @@ namespace Barotrauma.Items.Components updateTimer -= deltaTime; } - if (!ignoreDelay && waitTimer > 0) + if (AimDelay && waitTimer > 0) { waitTimer -= deltaTime; return; @@ -934,40 +1002,48 @@ namespace Barotrauma.Items.Components float shootDistance = AIRange; ISpatialEntity target = null; float closestDist = shootDistance * shootDistance; - if (targetHumans || targetOtherCreatures) + if (TargetCharacters) { foreach (var character in Character.CharacterList) { - if (character == null || character.Removed || character.IsDead) { continue; } - if (character.Params.Group == ai.Config.Entity) { continue; } - bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName; - if (isHuman) - { - if (!targetHumans) - { - // Don't target humans if not defined to. - continue; - } - } - else if (!targetOtherCreatures) - { - // Don't target other creatures if not defined to. - continue; - } + if (!IsValidTarget(character)) { continue; } + float priority = isSlowTurret ? character.Params.AISlowTurretPriority : character.Params.AITurretPriority; + if (priority <= 0) { continue; } + if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; } float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + if (!CheckTurretAngle(character.WorldPosition)) { continue; } target = character; - closestDist = dist; + closestDist = dist / priority; } } - if (targetSubmarines) + if (TargetItems) + { + foreach (Item targetItem in Item.ItemList) + { + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDist) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + target = targetItem; + closestDist = dist / priority; + } + } + if (TargetSubmarines) { if (target == null || target.Submarine != null) { closestDist = maxDistance * maxDistance; foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player) { continue; } + if (sub == Item.Submarine) { continue; } + if (item.Submarine != null) + { + if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; } + } float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } closestSub = sub; @@ -981,34 +1057,41 @@ namespace Barotrauma.Items.Components if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + // Don't check the angle, because it doesn't work on Thalamus spike. The angle check wouldn't be very important here anyway. target = hull; closestDist = dist; } } } } - if (!ignoreDelay) + + if (target == null && RandomMovement) { - if (target == null) + // Random movement while there's no target + waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); + targetRotation = Rand.Range(minRotation, maxRotation); + updatePending = true; + return; + } + + if (AimDelay) + { + if (RandomAimAmount > 0) { - // Random movement - waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); - targetRotation = Rand.Range(minRotation, maxRotation); - updatePending = true; - return; - } - if (disorderTimer < 0) - { - // Random disorder - disorderTimer = Rand.Range(0f, 3f); - waitTimer = Rand.Range(0.25f, 1f); - targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f)); - updatePending = true; - return; - } - else - { - disorderTimer -= deltaTime; + if (randomAimTimer < 0) + { + // Random disorder or other flaw in the targeting. + randomAimTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime); + waitTimer = Rand.Range(0.25f, 1f); + float randomAim = MathHelper.ToRadians(RandomAimAmount); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim)); + updatePending = true; + return; + } + else + { + randomAimTimer -= deltaTime; + } } } if (target == null) { return; } @@ -1043,11 +1126,11 @@ namespace Barotrauma.Items.Components start -= target.Submarine.SimPosition; end -= target.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines)); + shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)); } else { - shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines); + shoot = CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines); } if (shoot) { @@ -1055,7 +1138,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { @@ -1205,18 +1288,19 @@ namespace Barotrauma.Items.Components bool hadCurrentTarget = currentTarget != null; if (hadCurrentTarget) { - if (currentTarget.Removed || currentTarget.IsDead) + if (!IsValidTarget(currentTarget)) { currentTarget = null; + aiFindTargetTimer = CrewAIFindTargetMinInverval; } } - - if (aiFindTargetTimer <= 0.0f || currentTarget == null) + if (aiFindTargetTimer <= 0.0f) { foreach (Character enemy in Character.CharacterList) { - // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled) { continue; } + if (!IsValidTarget(enemy)) { continue; } + float priority = isSlowTurret ? enemy.Params.AISlowTurretPriority : enemy.Params.AITurretPriority; + if (priority <= 0) { continue; } if (character.Submarine != null) { if (enemy.Submarine == character.Submarine) { continue; } @@ -1233,30 +1317,53 @@ namespace Barotrauma.Items.Components // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } } + targetPos = enemy.WorldPosition; closestEnemy = enemy; - closestDistance = dist; + closestDistance = dist / priority; + currentTarget = closestEnemy; } - currentTarget = closestEnemy; - aiFindTargetTimer = aiFindTargetInterval; - } - else - { - closestEnemy = currentTarget; - } - - if (closestEnemy != null) - { - targetPos = closestEnemy.WorldPosition; - //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is - if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item)) + foreach (Item targetItem in Item.ItemList) { - targetPos = closestEnemy.CurrentHull.WorldPosition; + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDistance) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + targetPos = targetItem.WorldPosition; + closestDistance = dist / priority; + // Override the target character so that we can target the item instead. + closestEnemy = null; + currentTarget = targetItem; + } + if (currentTarget == null) + { + aiFindTargetTimer = CrewAIFindTargetMinInverval; + } + else + { + aiFindTargetTimer = CrewAiFindTargetMaxInterval; + } + } + else if (currentTarget != null) + { + targetPos = currentTarget.WorldPosition; + } + bool iceSpireSpotted = false; + // Adjust the target character position (limb or submarine) + if (currentTarget is Character targetCharacter) + { + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is + if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) + { + targetPos = targetCharacter.CurrentHull.WorldPosition; } 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; - foreach (Limb limb in closestEnemy.AnimController.Limbs) + foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } @@ -1270,13 +1377,14 @@ namespace Barotrauma.Items.Components } if (closestDist > shootDistance * shootDistance) { - // Not close enough to shoot + // Not close enough to shoot. + currentTarget = null; closestEnemy = null; targetPos = null; } } } - else if (item.Submarine != null && Level.Loaded != null) + else if (targetPos == null && item.Submarine != null && Level.Loaded != null) { // Check ice spires shootDistance = AIRange * item.OffsetOnSelectedMultiplier; @@ -1286,50 +1394,49 @@ namespace Barotrauma.Items.Components if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } foreach (var cell in wall.Cells) { - if (cell.DoesDamage) + if (!cell.DoesDamage) { continue; } + foreach (var edge in cell.Edges) { - foreach (var edge in cell.Edges) + Vector2 p1 = edge.Point1 + cell.Translation; + Vector2 p2 = edge.Point2 + cell.Translation; + Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); + if (!CheckTurretAngle(closestPoint)) { - Vector2 p1 = edge.Point1 + cell.Translation; - Vector2 p2 = edge.Point2 + cell.Translation; - Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); - if (!CheckTurretAngle(closestPoint)) + // The closest point can't be targeted -> get a point directly in front of the turret + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { - // The closest point can't be targeted -> get a point directly in front of the turret - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); - if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) - { - closestPoint = intersection; - if (!CheckTurretAngle(closestPoint)) { continue; } - } - else - { - continue; - } + closestPoint = intersection; + if (!CheckTurretAngle(closestPoint)) { continue; } } - float dist = Vector2.Distance(closestPoint, item.WorldPosition); - - //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell - closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); - - if (dist > AIRange + 1000) { continue; } - float dot = 0; - if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + else { - dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); - } - float minAngle = 0.5f; - if (dot < minAngle && dist > 1000) - { - // The sub is not moving towards the target and it's not very close to the turret either -> ignore continue; } - // Allow targeting farther when heading towards the spire (up to 1000 px) - dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); - if (dist > closestDistance) { continue; } - targetPos = closestPoint; - closestDistance = dist; } + float dist = Vector2.Distance(closestPoint, item.WorldPosition); + + //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell + closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); + + if (dist > AIRange + 1000) { continue; } + float dot = 0; + if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + { + dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); + } + float minAngle = 0.5f; + if (dot < minAngle && dist > 1000) + { + // The sub is not moving towards the target and it's not very close to the turret either -> ignore + continue; + } + // Allow targeting farther when heading towards the spire (up to 1000 px) + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); + if (dist > closestDistance) { continue; } + targetPos = closestPoint; + closestDistance = dist; + iceSpireSpotted = true; } } } @@ -1345,13 +1452,13 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) + if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) { character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, identifier: "newtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value, identifier: "identifiedtargetspotted".ToIdentifier(), @@ -1364,17 +1471,17 @@ namespace Barotrauma.Items.Components minDurationBetweenSimilar: 5.0f); } } - else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (!CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } - character.AddEncounter(closestEnemy); + CreatureMetrics.AddEncounter(closestEnemy.SpeciesName); } character.AIController.SelectTarget(closestEnemy.AiTarget); } - else if (closestEnemy == null && character.IsOnPlayerTeam) + else if (iceSpireSpotted && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogIceSpireSpotted").Value, identifier: "icespirespotted".ToIdentifier(), @@ -1437,7 +1544,55 @@ namespace Barotrauma.Items.Components return 0; } - private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) + // Not exahustive, but helps to get rid of some code duplication + private static bool IsValidTarget(ISpatialEntity target) + { + if (target == null) { return false; } + if (target is Character targetCharacter) + { + if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0) + { + return false; + } + } + else if (target is Item targetItem) + { + if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.HiddenInGame) + { + return false; + } + if (targetItem.Submarine != null) + { + return false; + } + } + return true; + } + + private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag) + { + if (!friendlyTag.IsEmpty) + { + if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; } + } + bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName; + if (isHuman) + { + if (item.Submarine != null) + { + // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP). + return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans; + } + return TargetHumans; + } + else + { + // Shouldn't check the team here, because all the enemies are in the same team (None). + return TargetMonsters; + } + } + + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) { if (targetBody == null) { return false; } Character targetCharacter = null; @@ -1449,7 +1604,7 @@ namespace Barotrauma.Items.Components { targetCharacter = limb.character; } - if (targetCharacter != null) + if (targetCharacter != null && !targetCharacter.Removed) { if (user != null) { @@ -1458,27 +1613,25 @@ namespace Barotrauma.Items.Components return false; } } - if (ai != null) + else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag)) { - if (targetCharacter.Params.Group == ai.Config.Entity) - { - return false; - } + // Note that Thalamus runs this even when AutoOperate is false. + return false; } } else { if (targetBody.UserData is ISpatialEntity e) { - if (e is Structure s && s.Indestructible) { return false; } - Submarine sub = e.Submarine ?? e as Submarine; + if (e is Structure { Indestructible: true }) { return false; } if (!targetSubmarines && e is Submarine) { return false; } - if (sub == null) { return false; } + Submarine sub = e.Submarine ?? e as Submarine; + if (sub == null) { return true; } if (sub == Item.Submarine) { return false; } if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } if (sub.TeamID == Item.Submarine.TeamID) { return false; } } - else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) + else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true }) { // Hit something else, probably a level wall return false; @@ -1489,7 +1642,7 @@ namespace Barotrauma.Items.Components private Body CheckLineOfSight(Vector2 start, Vector2 end) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionProjectile; Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, customPredicate: (Fixture f) => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index bd06a6ed3..c5a73d53f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn.")] public bool AllowUseWhenWorn { get; set; } public readonly int Variants; @@ -527,14 +527,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (picker.Removed) + if (picker == null || picker.Removed) { IsActive = false; return; } - item.SetTransform(picker.SimPosition, 0.0f); - + //if the item is also being held, let the Holdable component control the position + if (item.GetComponent() is not { IsActive: true }) + { + item.SetTransform(picker.SimPosition, 0.0f); + } item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index c31f6c337..5a08c3838 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -141,7 +141,18 @@ namespace Barotrauma } } if (items.Contains(item)) { return; } - items.Add(item); + + //keep lowest-condition items at the top of the stack + int index = 0; + for (int i = 0; i < items.Count; i++) + { + if (items[i].Condition > item.Condition) + { + break; + } + index++; + } + items.Insert(index, item); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 346f772dd..b4b5ef815 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -99,7 +99,18 @@ namespace Barotrauma private bool hasComponentsToDraw; public PhysicsBody body; - private float waterDragCoefficient; + private readonly float originalWaterDragCoefficient; + private float? overrideWaterDragCoefficient; + public float WaterDragCoefficient + { + get => overrideWaterDragCoefficient ?? originalWaterDragCoefficient; + set => overrideWaterDragCoefficient = value; + } + + /// + /// Removes the override value -> falls back to using the original value defined in the xml. + /// + public void ResetWaterDragCoefficient() => overrideWaterDragCoefficient = null; public readonly XElement StaticBodyConfig; @@ -142,6 +153,8 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; + public Action OnInteract; + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; @@ -423,8 +436,6 @@ namespace Barotrauma } } - public Color? HighlightColor; - /// /// Can be used by status effects or conditionals to check whether the item is contained inside something /// @@ -459,6 +470,8 @@ namespace Barotrauma } } + public Color? HighlightColor; + [Serialize("", IsPropertySaveable.Yes)] /// @@ -471,7 +484,8 @@ namespace Barotrauma { if (AiTarget != null) { - AiTarget.SonarLabel = !string.IsNullOrEmpty(value) && value.Length > 200 ? value.Substring(200) : value; + string trimmedStr = !string.IsNullOrEmpty(value) && value.Length > 250 ? value.Substring(250) : value; + AiTarget.SonarLabel = TextManager.Get(trimmedStr).Fallback(trimmedStr); } } } @@ -637,11 +651,13 @@ namespace Barotrauma } } - [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] - public bool AllowStealing + private bool allowStealing; + [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true, + description: $"Determined by where/how the item originally spawned. If ItemPrefab.AllowStealing is true, stealing the item is always allowed.")] + public bool AllowStealing { - get; - set; + get { return allowStealing || Prefab.AllowStealingAlways; } + set { allowStealing = value; } } private string originalOutpost; @@ -948,7 +964,7 @@ namespace Barotrauma { if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat)) { - DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategoryStr + ")"); + DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")"); } else { @@ -987,6 +1003,7 @@ namespace Barotrauma case "infectedsprite": case "damagedinfectedsprite": case "swappableitem": + case "skillrequirementhint": break; case "staticbody": StaticBodyConfig = subElement; @@ -1055,8 +1072,7 @@ namespace Barotrauma if (body != null) { body.Submarine = submarine; - waterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", - GetComponent() != null || GetComponent() != null ? 0.1f : 1.0f); + originalWaterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", 5.0f); } //cache connections into a dictionary for faster lookups @@ -1600,7 +1616,7 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { - if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (string.IsNullOrEmpty(conditional.TargetItemComponent)) { if (!conditional.Matches(this)) { return false; } } @@ -1608,7 +1624,7 @@ namespace Barotrauma { foreach (ItemComponent component in components) { - if (component.Name != conditional.TargetItemComponentName) { continue; } + if (component.Name != conditional.TargetItemComponent) { continue; } if (!conditional.Matches(component)) { return false; } } } @@ -1653,7 +1669,7 @@ namespace Barotrauma if (effect.TargetSlot > -1) { - if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; } + if (!OwnInventory.GetItemsAt(effect.TargetSlot).Contains(containedItem)) { continue; } } hasTargets = true; @@ -1708,8 +1724,15 @@ namespace Barotrauma { targets.AddRange(character.AnimController.Limbs.ToList()); } + if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null) + { + foreach (var characterLimb in character.AnimController.Limbs) + { + if (effect.targetLimbs.Contains(characterLimb.type)) { targets.Add(characterLimb); } + } + } } - if (effect.HasTargetType(StatusEffect.TargetType.Limb)) + if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb != null) { targets.Add(limb); } @@ -1724,7 +1747,7 @@ namespace Barotrauma { if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } - float damageAmount = attack.GetItemDamage(deltaTime); + float damageAmount = attack.GetItemDamage(deltaTime, Prefab.ItemDamageMultiplier); Condition -= damageAmount; if (damageAmount >= Prefab.OnDamagedThreshold) @@ -1735,7 +1758,7 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - private void SetCondition(float value, bool isNetworkEvent) + private void SetCondition(float value, bool isNetworkEvent, bool executeEffects = true) { if (!isNetworkEvent) { @@ -1758,16 +1781,22 @@ namespace Barotrauma //Flag connections to be updated as device is broken flagChangedConnections(connections); #if CLIENT - foreach (ItemComponent ic in components) - { - ic.PlaySound(ActionType.OnBroken); - ic.StopSounds(ActionType.OnActive); + if (executeEffects) + { + foreach (ItemComponent ic in components) + { + ic.PlaySound(ActionType.OnBroken); + ic.StopSounds(ActionType.OnActive); + } } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif // Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow. SetPreviousCondition(); - ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); + if (executeEffects) + { + ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); + } } else if (condition > 0.0f && prevCondition <= 0.0f) { @@ -1851,15 +1880,15 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - CreateStatusEvent(); + CreateStatusEvent(loadingRound: false); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } - public void CreateStatusEvent() + public void CreateStatusEvent(bool loadingRound) { - GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound)); } private bool isActive = true; @@ -1901,7 +1930,7 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) + if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) { bool shouldBeActive = true; foreach (var conditional in ic.IsActiveConditionals) @@ -1972,7 +2001,10 @@ namespace Barotrauma if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty) { - UpdateTransform(); + if (body.CollisionCategories != Category.None) + { + UpdateTransform(); + } if (CurrentHull == null && Level.Loaded != null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { Spawner?.AddItemToRemoveQueue(this); @@ -1991,8 +2023,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = IsInWater(); - bool waterProof = WaterProof; + inWater = IsInWater() && !WaterProof; if (inWater) { //the item has gone through the surface of the water @@ -2007,15 +2038,19 @@ namespace Barotrauma } Item container = this.Container; - while (!waterProof && container != null) + while (container != null) { - waterProof = container.WaterProof; + if (container.WaterProof) + { + inWater = false; + break; + } container = container.Container; } } if (hasWaterStatusEffects && condition > 0.0f) { - ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); + ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); } } else @@ -2141,7 +2176,7 @@ namespace Barotrauma Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront); float speed = frontVel.Length(); - float drag = speed * speed * waterDragCoefficient * volume * Physics.NeutralDensity; + float drag = speed * speed * WaterDragCoefficient * volume * Physics.NeutralDensity; //very small drag on active projectiles to prevent affecting their trajectories much if (body.FarseerBody.IsBullet) { drag *= 0.1f; } Vector2 dragVec = -frontVel / speed * drag; @@ -2628,12 +2663,14 @@ namespace Barotrauma if (user == Character.Controlled) { GUI.ForceMouseOn(null); } if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (ic.CanBeSelected && !(ic is Door)) { selected = true; } + if (ic.CanBeSelected && ic is not Door) { selected = true; } } } if (!picked) { return false; } + OnInteract?.Invoke(); + if (user != null) { if (user.SelectedItem == this) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 4ad6b2731..3130414a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -82,6 +82,13 @@ namespace Barotrauma private readonly struct ItemStatusEventData : IEventData { public EventType EventType => EventType.Status; + + public readonly bool LoadingRound; + + public ItemStatusEventData(bool loadingRound) + { + LoadingRound = loadingRound; + } } private readonly struct AssignCampaignInteractionEventData : IEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 761e84d24..5c1c5c573 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1,16 +1,33 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Barotrauma.Extensions; using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { + readonly struct SkillRequirementHint + { + public readonly Identifier Skill; + public readonly float Level; + public readonly LocalizedString SkillName; + + public LocalizedString GetFormattedText(int skillLevel, string levelColorTag) => + $"{SkillName} {Level} (‖color:{levelColorTag}‖{skillLevel}‖color:end‖)"; + + public SkillRequirementHint(ContentXElement element) + { + Skill = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Level = element.GetAttributeFloat("level", 0); + SkillName = TextManager.Get("skillname." + Skill); + } + } + readonly struct DeconstructItem { public readonly Identifier ItemIdentifier; @@ -315,6 +332,8 @@ namespace Barotrauma public readonly bool TransferOnlyOnePerContainer; public readonly bool AllowTransfersHere = true; + public readonly float MinLevelDifficulty, MaxLevelDifficulty; + public PreferredContainer(XElement element) { Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty()).ToImmutableHashSet(); @@ -330,6 +349,9 @@ namespace Barotrauma TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer); AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere); + MinLevelDifficulty = element.GetAttributeFloat(nameof(MinLevelDifficulty), float.MinValue); + MaxLevelDifficulty = element.GetAttributeFloat(nameof(MaxLevelDifficulty), float.MaxValue); + if (element.GetAttribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 @@ -443,6 +465,8 @@ namespace Barotrauma //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. public ImmutableArray PreferredContainers { get; private set; } + public ImmutableArray SkillRequirementHints { get; private set; } + public SwappableItem SwappableItem { get; @@ -651,6 +675,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool AllowSellingWhenBroken { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AllowStealingAlways { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool Indestructible { get; private set; } @@ -660,6 +687,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float ExplosionDamageMultiplier { get; private set; } + [Serialize(1f, IsPropertySaveable.No)] + public float ItemDamageMultiplier { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool DamagedByProjectiles { get; private set; } @@ -772,6 +802,18 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")] public float BotPriority { get; private set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowNameInHealthBar { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description:"Should the bots shoot at this item with turret or not? Disabled by default.")] + public bool IsAITurretTarget { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with turrets? Defaults to 1. Distance to the target affects the decision making.")] + public float AITurretPriority { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] + public float AISlowTurretPriority { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -874,6 +916,15 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, ConfigElement); LoadDescription(ConfigElement); + var skillRequirementHints = new List(); + foreach (var skillRequirementHintElement in ConfigElement.GetChildElements("SkillRequirementHint")) + { + skillRequirementHints.Add(new SkillRequirementHint(skillRequirementHintElement)); + } + if (skillRequirementHints.Any()) + { + SkillRequirementHints = skillRequirementHints.ToImmutableArray(); + } var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); @@ -1167,7 +1218,10 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + return + priceInfo is { CanBeBought: true } && + (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty && + (!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store?.Location.Faction?.Prefab.Identifier == p.Key || store?.Location.SecondaryFaction?.Prefab.Identifier == p.Key)); } public bool CanBeBoughtFrom(Location location) @@ -1179,6 +1233,15 @@ namespace Barotrauma if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; } + if (priceInfo.MinReputation.Any()) + { + if (!priceInfo.MinReputation.Any(p => + location?.Faction?.Prefab.Identifier == p.Key || + location?.SecondaryFaction?.Prefab.Identifier == p.Key)) + { + continue; + } + } return true; } return false; @@ -1335,11 +1398,43 @@ namespace Barotrauma } public Identifier VariantOf { get; } - + public ItemPrefab ParentPrefab { get; set; } + public void InheritFrom(ItemPrefab parent) { - ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML).FromPackage(ConfigElement.ContentPackage); ParseConfigElement(parent); + + void CheckXML(XElement originalElement, XElement variantElement, XElement result) + { + if (result == null) { return; } + if (result.Name.ToIdentifier() == "RequiredItem" && + result.Parent?.Name.ToIdentifier() == "Fabricate") + { + int originalAmount = originalElement.GetAttributeInt("amount", 1); + Identifier originalIdentifier = originalElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (variantElement == null) + { + //if the variant defines some fabrication requirements, we probably don't want to inherit anything extra from the base item? + if (this.originalElement.GetChildElement("Fabricate")?.GetChildElement("RequiredItem") != null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " + + $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements."); + } + return; + } + + Identifier resultIdentifier = result.GetAttributeIdentifier("identifier", Identifier.Empty); + if (originalAmount > 1 && variantElement.GetAttribute("amount") == null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " + + $"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+ + "Specify the amount in the variant to fix this."); + } + } + } } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 41e4f333c..c4647ef25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -8,44 +8,89 @@ using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Used by various features to define different kinds of relations between items: + /// for example, which item a character must have equipped to interact with some item in some way, + /// which items can go inside a container, or which kind of item the target of a status effect must have for the effect to execute. + /// class RelatedItem { public enum RelationType { None, + /// + /// The item must be contained inside the item this relation is defined in. + /// Can for example by used to make an item usable only when there's a specific kind of item inside it. + /// Contained, + /// + /// The user must have equipped the item (i.e. held or worn). + /// Equipped, + /// + /// The user must have picked up the item (i.e. the item needs to be in the user's inventory). + /// Picked, - Container + /// + /// The item this relation is defined in must be inside a specific kind of container. + /// Can for example by used to make an item do something when it's inside some other type of item. + /// + Container, + /// + /// Signifies an error (type could not be parsed) + /// + Invalid } - public bool IsOptional { get; set; } - + /// + /// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. + /// public bool MatchOnEmpty { get; set; } + /// + /// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. + /// public bool RequireEmpty { get; set; } + /// + /// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor, + /// making it easier to for example make rewire things that require some special tool to rewire. + /// public bool IgnoreInEditor { get; set; } + /// + /// Identifier(s) or tag(s) of the items that are NOT considered valid. + /// Can be used to, for example, exclude some specific items when using tags that apply to multiple items. + /// public ImmutableHashSet ExcludedIdentifiers { get; private set; } - private RelationType type; + private readonly RelationType type; - public List statusEffects; - + public List StatusEffects = new List(); + + /// + /// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). + /// public LocalizedString Msg; + + /// + /// Only valid for the RequiredItems of an ItemComponent. The localization tag of a message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). + /// public Identifier MsgTag; /// - /// Should broken (0 condition) items be excluded + /// Should broken (0 condition) items be excluded? /// public bool ExcludeBroken { get; private set; } /// - /// Should full condition (100%) items be excluded + /// Should full condition (100%) items be excluded? /// public bool ExcludeFullCondition { get; private set; } + /// + /// Are item variants considered valid? + /// public bool AllowVariants { get; private set; } = true; public RelationType Type @@ -59,19 +104,34 @@ namespace Barotrauma public int TargetSlot = -1; /// - /// Overrides the position defined in ItemContainer. + /// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer. /// public Vector2? ItemPos; /// - /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. /// - public bool? Hide; + public bool Hide; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Can be used to override the rotation of specific items in the container. + /// public float Rotation; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Can be used to force specific items to stay active inside the container (such as flashlights attached to a gun). + /// public bool SetActive; + /// + /// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional, + /// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door). + /// + public bool IsOptional { get; set; } + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -83,6 +143,9 @@ namespace Barotrauma } } + /// + /// Identifier(s) or tag(s) of the items that are considered valid. + /// public ImmutableHashSet Identifiers { get; private set; } public string JoinedExcludedIdentifiers @@ -139,8 +202,121 @@ namespace Barotrauma { this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); + } + + public RelatedItem(ContentXElement element, string parentDebugName) + { + Identifier[] identifiers; + if (element.GetAttribute("name") != null) + { + //backwards compatibility + a console warning + DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); + Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); + //attempt to convert to identifiers and tags + List convertedIdentifiers = new List(); + foreach (Identifier itemName in itemNames) + { + var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); + if (matchingItem != null) + { + convertedIdentifiers.Add(matchingItem.Identifier); + } + else + { + //no matching item found, this must be a tag + convertedIdentifiers.Add(itemName); + } + } + identifiers = convertedIdentifiers.ToArray(); + } + else + { + identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); + if (identifiers == null) + { + identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); + if (identifiers == null) + { + identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); + } + } + } + this.Identifiers = identifiers.ToImmutableHashSet(); + + Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); + } + } + this.ExcludedIdentifiers = excludedIdentifiers.ToImmutableHashSet(); + + ExcludeBroken = element.GetAttributeBool("excludebroken", true); + RequireEmpty = element.GetAttributeBool("requireempty", false); + ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false); + AllowVariants = element.GetAttributeBool("allowvariants", true); + Rotation = element.GetAttributeFloat("rotation", 0f); + SetActive = element.GetAttributeBool("setactive", false); + + if (element.GetAttribute(nameof(Hide)) != null) + { + Hide = element.GetAttributeBool(nameof(Hide), false); + } + if (element.GetAttribute(nameof(ItemPos)) != null) + { + ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); + } + string typeStr = element.GetAttributeString("type", ""); + if (string.IsNullOrEmpty(typeStr)) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "containable": + typeStr = "Contained"; + break; + case "suitablefertilizer": + case "suitableseed": + typeStr = "None"; + break; + } + } + if (!Enum.TryParse(typeStr, true, out type)) + { + DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); + type = RelationType.Invalid; + } + + MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); + LocalizedString msg = TextManager.Get(MsgTag); + if (!msg.Loaded) + { + Msg = MsgTag.Value; + } + else + { +#if CLIENT + foreach (InputType inputType in Enum.GetValues(typeof(InputType))) + { + msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); + } + Msg = msg; +#endif + } + + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } + StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + } + + IsOptional = element.GetAttributeBool("optional", false); + IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); + MatchOnEmpty = element.GetAttributeBool("matchonempty", false); + TargetSlot = element.GetAttributeInt("targetslot", -1); - statusEffects = new List(); } public bool CheckRequirements(Character character, Item parentItem) @@ -197,11 +373,14 @@ namespace Barotrauma bool isEmpty = parentItem.OwnInventory.IsEmpty(); if (RequireEmpty && !isEmpty) { return false; } if (MatchOnEmpty && isEmpty) { return true; } - foreach (Item contained in parentItem.ContainedItems) + foreach (var container in parentItem.GetComponents()) { - if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } - if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } - if (CheckContained(contained)) { return true; } + foreach (Item contained in container.Inventory.AllItems) + { + if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } + if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } + if (CheckContained(contained)) { return true; } + } } return false; } @@ -221,9 +400,9 @@ namespace Barotrauma new XAttribute("rotation", Rotation), new XAttribute("setactive", SetActive)); - if (Hide.HasValue) + if (Hide) { - element.Add(new XAttribute(nameof(Hide), Hide.Value)); + element.Add(new XAttribute(nameof(Hide), true)); } if (ItemPos.HasValue) { @@ -239,120 +418,10 @@ namespace Barotrauma } public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) - { - Identifier[] identifiers; - if (element.GetAttribute("name") != null) - { - //backwards compatibility + a console warning - DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); - Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); - //attempt to convert to identifiers and tags - List convertedIdentifiers = new List(); - foreach (Identifier itemName in itemNames) - { - var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); - if (matchingItem != null) - { - convertedIdentifiers.Add(matchingItem.Identifier); - } - else - { - //no matching item found, this must be a tag - convertedIdentifiers.Add(itemName); - } - } - identifiers = convertedIdentifiers.ToArray(); - } - else - { - identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); - if (identifiers == null) - { - identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); - if (identifiers == null) - { - identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); - } - } - } - - Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); - if (excludedIdentifiers == null) - { - excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); - if (excludedIdentifiers == null) - { - excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); - } - } - - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } - - RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) - { - ExcludeBroken = element.GetAttributeBool("excludebroken", true), - RequireEmpty = element.GetAttributeBool("requireempty", false), - ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), - AllowVariants = element.GetAttributeBool("allowvariants", true), - Rotation = element.GetAttributeFloat("rotation", 0f), - SetActive = element.GetAttributeBool("setactive", false) - }; - if (element.GetAttribute(nameof(Hide)) != null) - { - ri.Hide = element.GetAttributeBool(nameof(Hide), false); - } - if (element.GetAttribute(nameof(ItemPos)) != null) - { - ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); - } - string typeStr = element.GetAttributeString("type", ""); - if (string.IsNullOrEmpty(typeStr)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "containable": - typeStr = "Contained"; - break; - case "suitablefertilizer": - case "suitableseed": - typeStr = "None"; - break; - } - } - if (!Enum.TryParse(typeStr, true, out ri.type)) - { - DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); - return null; - } - - ri.MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); - LocalizedString msg = TextManager.Get(ri.MsgTag); - if (!msg.Loaded) - { - ri.Msg = ri.MsgTag.Value; - } - else - { -#if CLIENT - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); - } - ri.Msg = msg; -#endif - } - - foreach (var subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } - ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); - } - - ri.IsOptional = element.GetAttributeBool("optional", false); - ri.IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); - ri.MatchOnEmpty = element.GetAttributeBool("matchonempty", false); - ri.TargetSlot = element.GetAttributeInt("targetslot", -1); - + { + RelatedItem ri = new RelatedItem(element, parentDebugName); + if (ri.Type == RelationType.Invalid) { return null; } + if (ri.Identifiers.None() && ri.ExcludedIdentifiers.None() && !returnEmpty) { return null; } return ri; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 92effd495..59721d257 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -36,6 +36,10 @@ namespace Barotrauma public bool IdFreed { get; private set; } + /// + /// Unique, but non-persistent identifier. + /// Stays the same if the entities are created in the exactly same order, but doesn't persist e.g. between the rounds. + /// public readonly ushort ID; public virtual Vector2 SimPosition => Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index b22a9280e..2ce9bd0e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -127,6 +127,7 @@ namespace Barotrauma hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } + Attack.DamageMultiplier = 1.0f; float displayRange = Attack.Range; if (damageSource is Item sourceItem) { @@ -157,6 +158,10 @@ namespace Barotrauma Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f)); Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration); } + foreach (Item item in Item.ItemList) + { + item.GetComponent()?.RegisterExplosion(this, worldPosition); + } #endif if (displayRange < 0.1f) { return; } @@ -201,7 +206,7 @@ namespace Barotrauma powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } - static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - MathF.Sqrt(distSqr) / displayRange; } if (itemRepairStrength > 0.0f) @@ -210,7 +215,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); - if (distSqr > displayRangeSqr) continue; + if (distSqr > displayRangeSqr) { continue; } float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; //repair repairable items @@ -266,7 +271,7 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { float distFactor = 1.0f - dist / displayRange; - float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier); Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } @@ -354,7 +359,7 @@ namespace Barotrauma if (affliction.DivideByLimbCount) { float limbCountFactor = distFactors.Count; - if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. @@ -396,9 +401,12 @@ namespace Barotrauma if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); - var statusEffectTargets = new List() { c, limb }; + var statusEffectTargets = new List(); foreach (StatusEffect statusEffect in attack.StatusEffects) { + statusEffectTargets.Clear(); + if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffectTargets.Add(c); } + if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) { statusEffectTargets.Add(limb); } statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index b7f384cea..ed866eed9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -4,11 +4,12 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { - partial class Gap : MapEntity + partial class Gap : MapEntity, ISerializableEntity { public static List GapList = new List(); @@ -31,6 +32,8 @@ namespace Barotrauma /// public bool IsDiagonal { get; } + public readonly float GlowEffectT; + //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) private float open; @@ -51,7 +54,6 @@ namespace Barotrauma //can ambient light get through the gap even if it's not open public bool PassAmbientLight; - //a collider outside the gap (for example an ice wall next to the sub) //used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub private Body outsideCollisionBlocker; @@ -63,8 +65,43 @@ namespace Barotrauma set { if (float.IsNaN(value)) { return; } - if (value > open) { openedTimer = 1.0f; } + if (value > open) + { + openedTimer = 1.0f; + } + if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) + { + if (value > open && value >= 1.0f) + { + InformWaypointsAboutGapState(this, open: true); + } + else if (value < open && open >= 1.0f) + { + InformWaypointsAboutGapState(this, open: false); + } + } open = MathHelper.Clamp(value, 0.0f, 1.0f); + + static void InformWaypointsAboutGapState(Gap gap, bool open) + { + foreach (var wp in WayPoint.WayPointList) + { + if (IsWaypointRightAboveGap(gap, wp)) + { + wp.OnGapStateChanged(open, gap); + } + } + } + + static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp) + { + if (wp.SpawnType != SpawnType.Path) { return false; } + if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; } + if (wp.Position.Y < gap.Rect.Top) { return false; } + if (wp.Position.X > gap.Rect.Right) { return false; } + if (wp.Position.X < gap.Rect.Left) { return false; } + return true; + } } } @@ -118,12 +155,12 @@ namespace Barotrauma } } - public override string Name + public override string Name => "Gap"; + + public readonly Dictionary properties; + public Dictionary SerializableProperties { - get - { - return "Gap"; - } + get { return properties; } } public Gap(Rectangle rectangle) @@ -150,10 +187,14 @@ namespace Barotrauma IsDiagonal = isDiagonal; open = 1.0f; + properties = SerializableProperty.GetProperties(this); + FindHulls(); GapList.Add(this); InsertToList(); + GlowEffectT = Rand.Range(0.0f, 1.0f); + float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize, BodyType.Static, @@ -272,30 +313,51 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false); - if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); + if (hulls[i] == null) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); } } - if (hulls[0] == null && hulls[1] == null) { return; } + if (hulls[0] != null || hulls[1] != null) + { + if (hulls[0] == null && hulls[1] != null) + { + (hulls[1], hulls[0]) = (hulls[0], hulls[1]); + } - if (hulls[0] == null && hulls[1] != null) - { - Hull temp = hulls[0]; - hulls[0] = hulls[1]; - hulls[1] = temp; + flowTargetHull = hulls[0]; + + for (int i = 0; i < 2; i++) + { + if (hulls[i] == null) { continue; } + linkedTo.Add(hulls[i]); + if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); } + } } - flowTargetHull = hulls[0]; - - for (int i = 0; i < 2; i++) - { - if (hulls[i] == null) { continue; } - linkedTo.Add(hulls[i]); - if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this); - } + RefreshOutsideCollider(); } + private int updateCount; + public override void Update(float deltaTime, Camera cam) { + int updateInterval = 4; + float flowMagnitude = flowForce.LengthSquared(); + if (flowMagnitude < 1.0f) + { + //very sparse updates if there's practically no water moving + updateInterval = 8; + } + else if (linkedTo.Count == 2 && flowMagnitude > 10.0f) + { + //frequent updates if water is moving between hulls + updateInterval = 1; + } + + updateCount++; + if (updateCount < updateInterval) { return; } + deltaTime *= updateCount; + updateCount = 0; + flowForce = Vector2.Zero; outsideColliderRaycastTimer -= deltaTime; @@ -593,7 +655,12 @@ namespace Barotrauma public bool RefreshOutsideCollider() { - if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false; + if (outsideCollisionBlocker == null) { return false; } + if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull) + { + outsideCollisionBlocker.Enabled = false; + return false; + } if (outsideColliderRaycastTimer <= 0.0f) { @@ -740,8 +807,7 @@ namespace Barotrauma public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { - Rectangle rect = Rectangle.Empty; - + Rectangle rect; if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 3752320d8..c604cc10d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1083,7 +1083,7 @@ namespace Barotrauma if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open - if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index f48a4ec23..1aeb8ddd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -9,7 +9,7 @@ namespace Barotrauma Vector2 WorldPosition { get; } float Health { get; } - AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true); + AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true); public readonly struct AttackEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 1144fc52b..2cc1d7c27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,3 +1,4 @@ +using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; @@ -13,11 +14,14 @@ namespace Barotrauma public readonly LocalizedString Description; public readonly bool IsEndBiome; + public readonly int EndBiomeLocationCount; + public readonly float MinDifficulty; private readonly float maxDifficulty; public float ActualMaxDifficulty => maxDifficulty; public float AdjustedMaxDifficulty => maxDifficulty - 0.1f; + public readonly ImmutableHashSet AllowedZones; private readonly SubmarineAvailability? submarineAvailability; @@ -41,6 +45,8 @@ namespace Barotrauma element.GetAttributeString("description", "")); IsEndBiome = element.GetAttributeBool("endbiome", false); + EndBiomeLocationCount = Math.Max(1, element.GetAttributeInt("endbiomelocationcount", 1)); + AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index c80309932..b2aaf0964 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -65,28 +65,28 @@ namespace Barotrauma set { maxHeight = Math.Max(value, minHeight); } } - [Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(2, IsPropertySaveable.Yes, description: "Minimum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinBranchCount { get { return minBranchCount; } set { minBranchCount = Math.Max(value, 0); } } - [Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(4, IsPropertySaveable.Yes, description: "Maximum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxBranchCount { get { return maxBranchCount; } set { maxBranchCount = Math.Max(value, minBranchCount); } } - [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] + [Serialize(50, IsPropertySaveable.Yes, description: "Total amount of level objects in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int LevelObjectAmount { get; set; } - [Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "What portion of the empty cells in the cave should be turned into destructible walls? For example, 0.1 = 10%."), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] public float DestructibleWallRatio { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 72bbace41..eb1e44499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -16,6 +16,11 @@ namespace Barotrauma { partial class Level : Entity, IServerSerializable { + public enum PlacementType + { + Top, Bottom + } + public enum EventType { SingleDestructibleWall, @@ -61,6 +66,7 @@ namespace Barotrauma [Flags] public enum PositionType { + None = 0, MainPath = 0x1, SidePath = 0x2, Cave = 0x4, @@ -68,7 +74,8 @@ namespace Barotrauma Wreck = 0x10, BeaconStation = 0x20, Abyss = 0x40, - AbyssCave = 0x80 + AbyssCave = 0x80, + Outpost = 0x100, } public struct InterestingPosition @@ -413,6 +420,9 @@ namespace Barotrauma get { return LevelData.Type; } } + + public bool IsEndBiome => LevelData.Biome != null && LevelData.Biome.IsEndBiome; + /// /// Is there a loaded level set and is it an outpost? /// @@ -451,6 +461,19 @@ namespace Barotrauma borders = new Rectangle(Point.Zero, levelData.Size); } + public bool ShouldSpawnCrewInsideOutpost() + { + if (StartOutpost != null && + Type == LevelData.LevelType.Outpost && + (StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && + StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) + { + var reputation = GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation; + return reputation == null || reputation.NormalizedValue >= Reputation.HostileThreshold; + } + return false; + } + public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) { Debug.Assert(levelData.Biome != null); @@ -482,11 +505,8 @@ namespace Barotrauma EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); - if (LevelData.ForceOutpostGenerationParams == null) - { - StartLocation = startLocation; - EndLocation = endLocation; - } + StartLocation = startLocation; + EndLocation = endLocation; GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); @@ -889,6 +909,12 @@ namespace Barotrauma // remove unnecessary cells and create some holes at the bottom of the level //---------------------------------------------------------------------------------- + if (GenerationParams.NoLevelGeometry) + { + cells.ForEach(c => c.CellType = CellType.Removed); + cells.Clear(); + } + cells = cells.Except(pathCells).ToList(); //remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad cells.ForEachMod(c => @@ -1687,14 +1713,22 @@ namespace Barotrauma foreach (VoronoiCell cell in closeCells) { bool tooClose = false; - foreach (GraphEdge edge in cell.Edges) - { - if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || - Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || - MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + + if (cell.IsPointInsideAABB(position, margin: minDistance)) + { + tooClose = true; + } + else + { + foreach (GraphEdge edge in cell.Edges) { - tooClose = true; - break; + if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || + Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || + MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + { + tooClose = true; + break; + } } } if (tooClose) { tooCloseCells.Add(cell); } @@ -1779,6 +1813,7 @@ namespace Barotrauma if (AbyssArea.Height < islandSize.Y) { return; } + int createdCaves = 0; int islandCount = GenerationParams.AbyssIslandCount; for (int i = 0; i < islandCount; i++) { @@ -1808,7 +1843,11 @@ namespace Barotrauma break; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability) + bool createCave = + //force at least one abyss cave + (i == islandCount - 1 && createdCaves == 0) || + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability; + if (!createCave) { float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); @@ -1867,6 +1906,7 @@ namespace Barotrauma new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2), new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland))); AbyssIslands.Add(new AbyssIsland(islandArea, islandCells)); + createdCaves++; } } @@ -2942,13 +2982,13 @@ namespace Barotrauma } /// Used by clients to set the rotation for the resources - public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable targetCaves = null) + public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable targetCaves = null) { var allValidLocations = GetAllValidClusterLocations(); var placedResources = new List(); - rotation = 0.0f; - if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?! + // if there are no valid locations, don't place anything + if (allValidLocations.None()) { return placedResources; } // Make sure not to pick a spot that already has other level resources for (int i = allValidLocations.Count - 1; i >= 0; i--) @@ -2979,10 +3019,12 @@ namespace Barotrauma if (PositionsOfInterest.None(p => p.PositionType == positionType)) { + DebugConsole.AddWarning($"Failed to find a position of the type \"{positionType}\" for mission resources."); foreach (var validType in MineralMission.ValidPositionTypes) { if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType)) { + DebugConsole.AddWarning($"Placing in \"{validType}\" instead."); positionType = validType; break; } @@ -3042,7 +3084,6 @@ namespace Barotrauma } PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources); Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); - rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); return placedResources; static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath; @@ -3151,9 +3192,7 @@ namespace Barotrauma if (item.GetComponent() is Holdable h) { h.AttachToWall(); -#if CLIENT item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); -#endif } else if (item.body != null) { @@ -3234,7 +3273,8 @@ namespace Barotrauma { suitablePositions.RemoveAll(p => !filter(p)); } - if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) + if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) || + positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave)) { suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); } @@ -3399,8 +3439,7 @@ namespace Barotrauma bool closeEnough = false; foreach (VoronoiCell cell in wall.Cells) { - if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize && - Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize) + if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2)) { closeEnough = true; break; @@ -3553,6 +3592,13 @@ namespace Barotrauma var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); + SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) + { + Type = type + }; + + //place downwards by default + var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom; // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000); @@ -3573,7 +3619,7 @@ namespace Barotrauma attemptsLeft--; if (TryGetSpawnPoint(out spawnPoint)) { - success = TryPositionSub(subBorders, subName, ref spawnPoint); + success = TryPositionSub(subBorders, subName, placement, ref spawnPoint); if (success) { break; @@ -3594,10 +3640,6 @@ namespace Barotrauma { Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); tempSW.Restart(); - SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) - { - Type = type - }; Submarine sub = new Submarine(info); if (type == SubmarineType.Wreck) { @@ -3647,10 +3689,10 @@ namespace Barotrauma return null; } - bool TryPositionSub(Rectangle subBorders, string subName, ref Vector2 spawnPoint) - { + bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint) + { positions.Add(spawnPoint); - bool bottomFound = TryRaycastToBottom(subBorders, ref spawnPoint); + bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint); positions.Add(spawnPoint); bool leftSideBlocked = IsSideBlocked(subBorders, false); @@ -3658,21 +3700,21 @@ namespace Barotrauma int step = 5; if (rightSideBlocked && !leftSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, -step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step); } else if (leftSideBlocked && !rightSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, step); } else if (!bottomFound) { if (!leftSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, -step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step); } else if (!rightSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, step); } else { @@ -3702,14 +3744,14 @@ namespace Barotrauma } return !isBlocked && bottomFound; - bool TryMove(Rectangle subBorders, ref Vector2 spawnPoint, float amount) + bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount) { float maxMovement = 5000; float totalAmount = 0; - bool foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); while (!IsSideBlocked(subBorders, amount > 0)) { - foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); totalAmount += amount; spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); if (Math.Abs(totalAmount) > maxMovement) @@ -3738,7 +3780,7 @@ namespace Barotrauma return false; } - bool TryRaycastToBottom(Rectangle subBorders, ref Vector2 spawnPoint) + bool TryRaycast(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint) { // Shoot five rays and pick the highest hit point. int rayCount = 5; @@ -3764,16 +3806,18 @@ namespace Barotrauma break; } var simPos = ConvertUnits.ToSimUnits(rayStart); - var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), - customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body), + var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1), + customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); if (body != null) { - positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, subBorders.Height / 2); + positions[i] = + ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + + new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1)); hit = true; } } - float highestPoint = positions.Max(p => p.Y); + float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y); spawnPoint = new Vector2(spawnPoint.X, highestPoint); return hit; } @@ -3953,8 +3997,18 @@ namespace Barotrauma if (LevelData.OutpostGenerationParamsExist) { Location location = i == 0 ? StartLocation : EndLocation; - OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ?? - LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient); + OutpostGenerationParams outpostGenerationParams = null; + if (LevelData.ForceOutpostGenerationParams != null) + { + outpostGenerationParams = LevelData.ForceOutpostGenerationParams; + } + else + { + outpostGenerationParams = + LevelData.ForceOutpostGenerationParams ?? + LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient); + } + LocationType locationType = location?.Type; if (locationType == null) { @@ -4030,52 +4084,70 @@ namespace Barotrauma } } - DockingPort outpostPort = null; - closestDistance = float.MaxValue; - foreach (DockingPort port in DockingPort.List) + Vector2 spawnPos; + if (GenerationParams.ForceOutpostPosition != Vector2.Zero) { - if (port.IsHorizontal || port.Docked) { continue; } - if (port.Item.Submarine != outpost) { continue; } - //the outpost port has to be at the bottom of the outpost - if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; } - float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X); - if (dist < closestDistance) + spawnPos = new Vector2(Size.X * GenerationParams.ForceOutpostPosition.X, Size.Y * GenerationParams.ForceOutpostPosition.Y); + } + else + { + DockingPort outpostPort = null; + closestDistance = float.MaxValue; + foreach (DockingPort port in DockingPort.List) { - outpostPort = port; - closestDistance = dist; + if (port.IsHorizontal || port.Docked) { continue; } + if (port.Item.Submarine != outpost) { continue; } + //the outpost port has to be at the bottom of the outpost + if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; } + float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X); + if (dist < closestDistance) + { + outpostPort = port; + closestDistance = dist; + } } - } - float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; - //don't try to compensate if the port is very far from the sub's center of mass - if (Math.Abs(subDockingPortOffset) > 5000.0f) - { - subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; - DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); - } - - float? outpostDockingPortOffset = null; - if (outpostPort != null) - { - outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; - //don't try to compensate if the port is very far from the outpost's center of mass - if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; + //don't try to compensate if the port is very far from the sub's center of mass + if (Math.Abs(subDockingPortOffset) > 5000.0f) { - outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); + string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + + float? outpostDockingPortOffset = null; + if (outpostPort != null) + { + outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; + //don't try to compensate if the port is very far from the outpost's center of mass + if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + { + outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); + string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + DebugConsole.NewMessage(warningMsg, Color.Orange); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + } + + spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, 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); } } - Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, 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); - } outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); + + foreach (WayPoint wp in WayPoint.WayPointList) + { + if (wp.Submarine == outpost && wp.SpawnType != SpawnType.Path) + { + PositionsOfInterest.Add(new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost)); + } + } + if ((i == 0) == !Mirrored) { StartOutpost = outpost; @@ -4094,13 +4166,12 @@ namespace Barotrauma outpost.Info.Name = EndLocation.Name; } } - } } private void CreateBeaconStation() { - if (!LevelData.HasBeaconStation) { return; } + if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } var beaconStationFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); @@ -4111,27 +4182,40 @@ namespace Barotrauma } var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon); - for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + ContentFile contentFile = null; + if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - var beaconStationFile = beaconStationFiles[i]; - var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); - Debug.Assert(matchingInfo != null); - if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + if (contentFile == null) { - if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) - { - beaconStationFiles.RemoveAt(i); - } + DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); } } - if (beaconStationFiles.None()) - { - DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); - return; - } - var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); - string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); + if (contentFile == null) + { + for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + { + var beaconStationFile = beaconStationFiles[i]; + var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + { + if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) + { + beaconStationFiles.RemoveAt(i); + } + } + } + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); + return; + } + contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); + } + + string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); if (BeaconStation == null) { @@ -4195,7 +4279,7 @@ namespace Barotrauma { bool allowDisconnectedWires = true; bool allowDamagedWalls = true; - if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info) + if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) { allowDisconnectedWires = info.AllowDisconnectedWires; allowDamagedWalls = info.AllowDamagedWalls; @@ -4346,7 +4430,7 @@ namespace Barotrauma corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; - selectedPrefab.GiveItems(corpse, wreck); + selectedPrefab.GiveItems(corpse, wreck, sp); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200)); bool applyBurns = Rand.Value() < 0.1f; @@ -4377,7 +4461,6 @@ namespace Barotrauma } } corpse.CharacterHealth.ForceUpdateVisuals(); - corpse.GiveIdCardTags(sp); bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index f926af7ca..b7a5aea58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -24,7 +24,7 @@ namespace Barotrauma public readonly Biome Biome; - public readonly LevelGenerationParams GenerationParams; + public LevelGenerationParams GenerationParams { get; private set; } public bool HasBeaconStation; public bool IsBeaconActive; @@ -60,12 +60,14 @@ namespace Barotrauma /// /// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of . /// - public readonly List EventHistory = new List(); + public readonly List EventHistory = new List(); /// /// Events that have already triggered in this level and can never trigger again. . /// - public readonly List NonRepeatableEvents = new List(); + public readonly List NonRepeatableEvents = new List(); + + public readonly Dictionary FinishedEvents = new Dictionary(); /// /// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. . @@ -150,10 +152,51 @@ namespace Barotrauma } string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty()); - EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); + EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); - NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); + NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); + + string finishedEventsName = nameof(FinishedEvents); + if (element.GetChildElement(finishedEventsName) is { } finishedEventsElement) + { + foreach (var childElement in finishedEventsElement.GetChildElements(finishedEventsName)) + { + Identifier eventSetIdentifier = childElement.GetAttributeIdentifier("set", Identifier.Empty); + if (eventSetIdentifier.IsEmpty) { continue; } + if (!EventSet.Prefabs.TryGet(eventSetIdentifier, out EventSet eventSet)) + { + foreach (var prefab in EventSet.Prefabs) + { + if (FindSetRecursive(prefab, eventSetIdentifier) is { } foundSet) + { + eventSet = foundSet; + break; + } + } + } + if (eventSet is null) { continue; } + int count = childElement.GetAttributeInt("count", 0); + if (count < 1) { continue; } + FinishedEvents.Add(eventSet, count); + } + + static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier) + { + foreach (var childSet in parentSet.ChildSets) + { + if (childSet.Identifier == setIdentifier) + { + return childSet; + } + if (FindSetRecursive(childSet, setIdentifier) is { } foundSet) + { + return foundSet; + } + } + return null; + } + } EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } @@ -163,7 +206,7 @@ namespace Barotrauma /// public LevelData(LocationConnection locationConnection) { - Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; + Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; Difficulty = locationConnection.Difficulty; @@ -196,9 +239,9 @@ namespace Barotrauma /// /// Instantiates level data using the properties of the location /// - public LevelData(Location location, float difficulty) + public LevelData(Location location, Map map, float difficulty) { - Seed = location.BaseName; + Seed = location.BaseName + map.Locations.IndexOf(location); Biome = location.Biome; Type = LevelType.Outpost; Difficulty = difficulty; @@ -254,14 +297,22 @@ namespace Barotrauma return levelData; } + public void ReassignGenerationParams(string seed) + { + GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier); + } public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any(); - public static IEnumerable GetSuitableOutpostGenerationParams(Location location) + public static IEnumerable GetSuitableOutpostGenerationParams(Location location, LevelData levelData) { - var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); + var suitableParams = OutpostGenerationParams.OutpostParams + .Where(p => p.LevelType == null || levelData.Type == p.LevelType) + .Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); if (!suitableParams.Any()) { - suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); + suitableParams = OutpostGenerationParams.OutpostParams + .Where(p => p.LevelType == null || levelData.Type == p.LevelType) + .Where(p => location == null || !p.AllowedLocationTypes.Any()); if (!suitableParams.Any()) { DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); @@ -305,11 +356,23 @@ namespace Barotrauma { if (EventHistory.Any()) { - newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier)))); + newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory))); } if (NonRepeatableEvents.Any()) { - newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier)))); + newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents))); + } + if (FinishedEvents.Any()) + { + var finishedEventsElement = new XElement(nameof(FinishedEvents)); + foreach (var (set, count) in FinishedEvents) + { + var element = new XElement(nameof(FinishedEvents), + new XAttribute("set", set.Identifier), + new XAttribute("count", count)); + finishedEventsElement.Add(element); + } + newElement.Add(finishedEventsElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 498702fa1..1a1210460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -67,7 +67,7 @@ namespace Barotrauma set; } - [Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(100.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float Commonness { get; @@ -95,7 +95,7 @@ namespace Barotrauma set; } - [Serialize("20,40,50", IsPropertySaveable.Yes), Editable()] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] public Color BackgroundTextureColor { get; @@ -116,6 +116,13 @@ namespace Barotrauma set; } + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] + public Color WaterParticleColor + { + get; + set; + } + private Vector2 startPosition; [Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition @@ -142,6 +149,19 @@ namespace Barotrauma } } + private Vector2 forceOutpostPosition; + [Serialize("0,0", IsPropertySaveable.Yes, "Position of the outpost (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner). If set to 0,0, the outpost is placed in a suitable position automatically."), Editable(DecimalCount = 2)] + public Vector2 ForceOutpostPosition + { + get { return forceOutpostPosition; } + set + { + forceOutpostPosition = new Vector2( + MathHelper.Clamp(value.X, 0.0f, 1.0f), + MathHelper.Clamp(value.Y, 0.0f, 1.0f)); + } + } + [Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] public bool CreateHoleNextToEnd { @@ -156,6 +176,13 @@ namespace Barotrauma set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, no walls generate in the level. Can be useful for e.g. levels that are just supposed to consist of a pre-built outpost."), Editable] + public bool NoLevelGeometry + { + get; + set; + } + [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { @@ -191,21 +218,21 @@ namespace Barotrauma set { height = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes, description: "Minimum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMin { get { return initialDepthMin; } set { initialDepthMin = Math.Max(value, 0); } } - [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes, description: "Maximum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMax { get { return initialDepthMax; } set { initialDepthMax = Math.Max(value, initialDepthMin); } } - [Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] + [Serialize(6500, IsPropertySaveable.Yes, description: "Minimum width of the main tunnel going through the level, in pixels. Can be automatically increased by the level editor if the submarine is larger than this."), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] public int MinTunnelRadius { get; @@ -213,7 +240,7 @@ namespace Barotrauma } - [Serialize("0,1", IsPropertySaveable.Yes), Editable] + [Serialize("0,1", IsPropertySaveable.Yes, description: "Amount of side tunnels in the level (min,max)."), Editable] public Point SideTunnelCount { get; @@ -221,14 +248,14 @@ namespace Barotrauma } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float SideTunnelVariance { get; set; } - [Serialize("2000,6000", IsPropertySaveable.Yes), Editable] + [Serialize("2000,6000", IsPropertySaveable.Yes, description: "Minimum width of the side tunnels, in pixels. Unlike the main tunnel, does not get adjusted based on the size of the submarine."), Editable] public Point MinSideTunnelRadius { get; @@ -309,7 +336,7 @@ namespace Barotrauma } } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float MainPathVariance { get; @@ -358,28 +385,28 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float CaveResourceSpawnChance { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of floating, destructible ice chunks in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int FloatingIceChunkCount { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of islands (static wall chunks along the main path) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100)] public int IslandCount { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of ice spires in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int IceSpireCount { get; set; } - [Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes, description: "Number of abyss islands in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int AbyssIslandCount { get; @@ -400,7 +427,7 @@ namespace Barotrauma set; } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable()] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "The probability of an abyss island having a cave. There is always a cave in at least one of the islands regardless of this setting."), Editable()] public float AbyssIslandCaveProbability { get; @@ -478,14 +505,14 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } + // TODO: Move the wreck parameters under a separate class? +#region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinWreckCount { get; set; } [Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxWreckCount { get; set; } - // TODO: Move the wreck parameters under a separate class? -#region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MinCorpseCount { get; set; } @@ -503,7 +530,10 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMaxWaterPercentage { get; set; } -#endregion + #endregion + + [Serialize("", IsPropertySaveable.Yes, description: "Should a beacon station always spawn in this type of level?")] + public string ForceBeaconStation { get; set; } [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability @@ -519,6 +549,14 @@ namespace Barotrauma private set { waterParticleScale = Math.Max(value, 0.01f); } } + private Vector2 waterParticleVelocity; + [Serialize("0,10", IsPropertySaveable.Yes, description: "How fast the water particle texture scrolls."), Editable] + public Vector2 WaterParticleVelocity + { + get { return waterParticleVelocity; } + private set { waterParticleVelocity = value; } + } + [Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallTextureSize { @@ -533,6 +571,34 @@ namespace Barotrauma private set; } + [Serialize("0,0", IsPropertySaveable.Yes, description: "Interval of lightning-like flashes of light in the level."), Editable] + public Vector2 FlashInterval + { + get; + set; + } + + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Color of lightning-like flashes of light in the level."), Editable] + public Color FlashColor + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should the \"ambient noise\" of the biome play in this level if it's an outpost level."), Editable] + public bool PlayNoiseLoopInOutpostLevel + { + get; + set; + } + + [Serialize(1.0f, IsPropertySaveable.Yes), Editable] + public float WaterAmbienceVolume + { + get; + set; + } + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] public float WallEdgeExpandOutwardsAmount { @@ -556,8 +622,13 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } +#if CLIENT + public Sounds.Sound FlashSound { get; private set; } +#endif + #warning TODO: this should be in the unit test project (#3164) public static void CheckValidity() + { foreach (Biome biome in Biome.Prefabs) { @@ -575,7 +646,7 @@ namespace Barotrauma } } - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default) + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -590,14 +661,29 @@ namespace Barotrauma lp.Type == type && (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); - matchingLevelParams = biome.IsEmpty - ? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)) - : matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome)); + if (biomeId.IsEmpty) + { + //we don't want end levels when generating a completely random level (e.g. in mission mode) + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)); + } + else + { + bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome; + if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId))) + { + //in the end biome, we must choose level parameters meant specifically for the end levels + matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + else + { + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + } if (!matchingLevelParams.Any()) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); - if (!biome.IsEmpty) + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); + if (!biomeId.IsEmpty) { //try to find params that at least have a suitable type matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type); @@ -611,7 +697,7 @@ namespace Barotrauma if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty)) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); } else { @@ -661,6 +747,11 @@ namespace Barotrauma case "waterparticles": WaterParticles = new Sprite(subElement); break; +#if CLIENT + case "flashsound": + FlashSound = GameMain.SoundManager.LoadSound(subElement); + break; +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 5b9d587fc..1bec94d04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -235,9 +235,22 @@ namespace Barotrauma SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level, cave); - if (prefab.MaxCount < amount) + if (amount > prefab.MaxCount && objects.Count > prefab.MaxCount) { - if (objects.Count(o => o.Prefab == prefab && o.ParentCave == cave) >= prefab.MaxCount) + bool maxReached = false; + int objectCount = 0; + for (int j = 0; j < objects.Count; j++) + { + if (objects[j].Prefab == prefab && objects[j].ParentCave == cave) + { + objectCount++; + if (objectCount >= prefab.MaxCount) + { + break; + } + } + } + if (objectCount >= prefab.MaxCount) { availablePrefabs.Remove(prefab); } @@ -350,8 +363,8 @@ namespace Barotrauma private void AddObject(LevelObject newObject, Level level) { - if (newObject.Triggers != null) - { + if (newObject.Triggers != null) + { foreach (LevelTrigger trigger in newObject.Triggers) { trigger.OnTriggered += (levelTrigger, obj) => @@ -360,7 +373,7 @@ namespace Barotrauma }; } } - + var spriteCorners = new List { Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero @@ -393,11 +406,11 @@ namespace Barotrauma } } - float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z; - float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z; + float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z / 10000.0f; + float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z / 10000.0f; - float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos; - float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos; + float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z / 10000.0f - level.BottomPos; + float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z / 10000.0f - level.BottomPos; if (newObject.Triggers != null) { @@ -436,11 +449,11 @@ namespace Barotrauma int xStart = (int)Math.Floor(minX / GridSize); int xEnd = (int)Math.Floor(maxX / GridSize); - if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) return; + if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) { return; } int yStart = (int)Math.Floor(minY / GridSize); int yEnd = (int)Math.Floor(maxY / GridSize); - if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) return; + if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) { return; } xStart = Math.Max(xStart, 0); xEnd = Math.Min(xEnd, objectGrid.GetLength(0) - 1); @@ -451,13 +464,21 @@ namespace Barotrauma { for (int y = yStart; y <= yEnd; y++) { - if (objectGrid[x, y] == null) objectGrid[x, y] = new List(); - objectGrid[x, y].Add(newObject); + var list = objectGrid[x, y]; + if (objectGrid[x, y] == null) { list = objectGrid[x, y] = new List(); } + + //insertion sort in ascending order (= prefer rendering objects in front) + int drawOrderIndex = 0; + while (drawOrderIndex < list.Count && list[drawOrderIndex].Position.Z < newObject.Position.Z) + { + drawOrderIndex++; + } + list.Insert(drawOrderIndex, newObject); } } } - public Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition) + public static Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition) { return new Microsoft.Xna.Framework.Point( (int)Math.Floor(worldPosition.X / GridSize), @@ -500,7 +521,7 @@ namespace Barotrauma return objectsInRange; } - private List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) + private static List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); @@ -593,12 +614,12 @@ namespace Barotrauma if (obj == triggeredObject || obj.Triggers == null) { continue; } foreach (LevelTrigger otherTrigger in obj.Triggers) { - otherTrigger.OtherTriggered(triggeredObject, trigger); + otherTrigger.OtherTriggered(trigger, triggerer); } } } - private LevelObjectPrefab GetRandomPrefab(Level level, IList availablePrefabs) + private static LevelObjectPrefab GetRandomPrefab(Level level, IList availablePrefabs) { if (availablePrefabs.Sum(p => p.GetCommonness(level.LevelData)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( @@ -606,7 +627,7 @@ namespace Barotrauma availablePrefabs.Select(p => p.GetCommonness(level.LevelData)).ToList(), Rand.RandSync.ServerAndClient); } - private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) + private static LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) { if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( @@ -634,7 +655,7 @@ namespace Barotrauma public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (!(extraData is EventData eventData)) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); } + if (extraData is not EventData eventData) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); } LevelObject obj = eventData.LevelObject; msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count); obj.ServerWrite(msg, c); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 213fad7bf..60d7ce783 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -129,7 +129,7 @@ namespace Barotrauma private set; } - [Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable] + [Serialize("0.0,1.0", IsPropertySaveable.Yes, description: "The sprite depth of the object (min, max). Values of 0 or less make the object render in front of walls, values larger than 0 make it render behind walls with a parallax effect."), Editable] public Vector2 DepthRange { get; @@ -273,14 +273,14 @@ namespace Barotrauma private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the object disappear if the object is destroyed? Only relevant if TakeLevelWallDamage is true."), Editable] public bool HideWhenBroken { get; private set; } - [Serialize(100.0f, IsPropertySaveable.Yes), Editable] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Amount of health the object has. Only relevant if TakeLevelWallDamage is true."), Editable] public float Health { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index a5c1b3f85..78deb6fe1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; @@ -215,7 +216,7 @@ namespace Barotrauma PhysicsBody.FarseerBody.SetIsSensor(element.GetAttributeBool("sensor", true)); PhysicsBody.FarseerBody.BodyType = BodyType.Static; - ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); + ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); } @@ -470,7 +471,7 @@ namespace Barotrauma /// /// Another trigger was triggered, check if this one should react to it /// - public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger) + public void OtherTriggered(LevelTrigger otherTrigger, Entity triggerer) { if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; } @@ -486,7 +487,16 @@ namespace Barotrauma triggeredTimer = stayTriggeredDelay; if (!wasAlreadyTriggered) { - OnTriggered?.Invoke(this, null); + if (!IsTriggeredByEntity(triggerer, triggeredBy, mustBeOutside: true)) { return; } + if (!triggerers.Contains(triggerer)) + { + if (!IsTriggered) + { + OnTriggered?.Invoke(this, triggerer); + } + TriggererPosition[triggerer] = triggerer.WorldPosition; + triggerers.Add(triggerer); + } } } } @@ -658,6 +668,10 @@ namespace Barotrauma { effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); } + else if (triggerer is Submarine sub) + { + effect.Apply(effect.type, deltaTime, sub, Array.Empty(), position); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); @@ -747,7 +761,7 @@ namespace Barotrauma Vector2 baseVel = GetWaterFlowVelocity(); if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero; - float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); + float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); float dist = Vector2.Distance(viewPosition, WorldPosition); if (dist > triggerSize) return Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 636c47c1a..58ae53c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -476,7 +476,7 @@ namespace Barotrauma bool leaveBehind = false; if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine)) { - System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); + System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEitherExit); if (Submarine.MainSub.AtEndExit) { leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7fa9b6fc2..6d13e794e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -3,7 +3,6 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -63,10 +62,17 @@ namespace Barotrauma public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; + public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false; + public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; public int LocationTypeChangeCooldown; + /// + /// Is some mission blocking this location from changing its type? + /// + public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + public string BaseName { get => baseName; } public string Name { get; private set; } @@ -83,7 +89,11 @@ namespace Barotrauma public int PortraitId { get; private set; } - public Reputation Reputation { get; set; } + public Faction Faction { get; set; } + + public Faction SecondaryFaction { get; set; } + + public Reputation Reputation => Faction?.Reputation; public int TurnsInRadiation { get; set; } @@ -92,6 +102,7 @@ namespace Barotrauma public class StoreInfo { public Identifier Identifier { get; } + public Identifier MerchantFaction { get; private set; } public int Balance { get; set; } public List Stock { get; } = new List(); public List DailySpecials { get; } = new List(); @@ -101,6 +112,7 @@ namespace Barotrauma /// public int PriceModifier { get; set; } public Location Location { get; } + private float MaxReputationModifier => Location.StoreMaxReputationModifier; private StoreInfo(Location location) { @@ -125,6 +137,7 @@ namespace Barotrauma public StoreInfo(Location location, XElement storeElement) : this(location) { Identifier = storeElement.GetAttributeIdentifier("identifier", ""); + MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), ""); Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance); PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute @@ -281,15 +294,17 @@ namespace Barotrauma { price = Location.DailySpecialPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(true); + // Adjust by current reputation + price *= GetReputationModifier(true); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { - if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) + var faction = GetMerchantOrLocationFactionIdentifier(); + if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { - price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated)); + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); + 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)); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); @@ -312,8 +327,8 @@ namespace Barotrauma { price = Location.RequestGoodPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(false); + // Adjust by location reputation + price *= GetReputationModifier(false); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) @@ -326,6 +341,45 @@ namespace Barotrauma return Math.Max((int)price, 1); } + public void SetMerchantFaction(Identifier factionIdentifier) + { + MerchantFaction = factionIdentifier; + } + + public Identifier GetMerchantOrLocationFactionIdentifier() + { + return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty); + } + + public float GetReputationModifier(bool buying) + { + var factionIdentifier = GetMerchantOrLocationFactionIdentifier(); + var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation; + if (reputation == null) { return 1.0f; } + if (buying) + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + else + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + } + public override string ToString() { return Identifier.Value; @@ -387,6 +441,8 @@ namespace Barotrauma } } + + public void SelectMission(Mission mission) { if (!SelectedMissions.Contains(mission) && mission != null) @@ -449,17 +505,22 @@ namespace Barotrauma public bool IsGateBetweenBiomes; - private struct LoadedMission + private readonly struct LoadedMission { - public MissionPrefab MissionPrefab { get; } - public int DestinationIndex { get; } - public bool SelectedMission { get; } + public readonly MissionPrefab MissionPrefab; + public readonly int TimesAttempted; + public readonly int OriginLocationIndex; + public readonly int DestinationIndex; + public readonly bool SelectedMission; - public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission) + public LoadedMission(XElement element) { - MissionPrefab = prefab; - DestinationIndex = destinationIndex; - SelectedMission = selectedMission; + var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty); + MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null; + TimesAttempted = element.GetAttributeInt("timesattempted", 0); + OriginLocationIndex = element.GetAttributeInt("origin", -1); + DestinationIndex = element.GetAttributeInt("destinationindex", -1); + SelectedMission = element.GetAttributeBool("selected", false); } } @@ -484,7 +545,7 @@ namespace Barotrauma /// /// Create a location from save data /// - public Location(XElement element) + public Location(CampaignMode campaign, XElement element) { Identifier locationTypeId = element.GetAttributeIdentifier("type", ""); bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type); @@ -497,12 +558,23 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); - IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); - MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); - TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); - StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); + IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); + MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); + StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + + var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty); + if (!factionIdentifier.IsEmpty) + { + Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + } + var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); + if (!secondaryFactionIdentifier.IsEmpty) + { + SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); + } Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty); if (biomeId != Identifier.Empty) { @@ -640,13 +712,11 @@ namespace Barotrauma loadedMissions = new List(); foreach (XElement childElement in missionsElement.GetChildElements("mission")) { - var id = childElement.GetAttributeString("prefabid", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var destination = childElement.GetAttributeInt("destinationindex", -1); - var selected = childElement.GetAttributeBool("selected", false); - loadedMissions.Add(new LoadedMission(prefab, destination, selected)); + var loadedMission = new LoadedMission(childElement); + if (loadedMission.MissionPrefab != null) + { + loadedMissions.Add(loadedMission); + } } } } @@ -656,7 +726,7 @@ namespace Barotrauma return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } - public void ChangeType(LocationType newType, bool createStores = true) + public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true) { if (newType == Type) { return; } @@ -671,56 +741,61 @@ namespace Barotrauma Type = newType; Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); - if (Type.MissionIdentifiers.Any()) + if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced()); + if (Faction == null) + { + Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced); + } + if (SecondaryFaction == null) + { + SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); + } } - if (Type.MissionTags.Any()) + else { - UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); + Faction = null; + SecondaryFaction = null; } + UnlockInitialMissions(Rand.RandSync.Unsynced); + if (createStores) { CreateStores(force: true); } } - public void UnlockInitialMissions() + public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync)); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient)); + UnlockMissionByTag(Type.MissionTags.GetRandom(randSync)); } } public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab, connection); - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } + AddMission(InstantiateMission(missionPrefab, connection)); } public void UnlockMission(MissionPrefab missionPrefab) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab); - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } + AddMission(InstantiateMission(missionPrefab)); } public Mission UnlockMissionByIdentifier(Identifier identifier) { if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) @@ -735,43 +810,45 @@ namespace Barotrauma { return null; } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); + DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true); return mission; } return null; } - public Mission UnlockMissionByTag(Identifier tag) + public Mission UnlockMissionByTag(Identifier tag, Random random = null) { - var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag)); - if (!matchingMissions.Any()) + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } + var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag)); + if (matchingMissions.None()) { DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found."); } else { - var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m)); + var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m)); if (unusedMissions.Any()) { var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this))); - if (!suitableMissions.Any()) + if (suitableMissions.None()) { suitableMissions = unusedMissions; } - MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced); + + MissionPrefab missionPrefab = + random != null ? + ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) : + ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, Rand.RandSync.Unsynced); + var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1]))) { return null; } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); + DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\".", debugOnly: true); return mission; } else @@ -783,6 +860,20 @@ namespace Barotrauma return null; } + private void AddMission(Mission mission) + { + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } + availableMissions.Add(mission); +#if CLIENT + GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); +#else + (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); +#endif + } + private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { if (prefab.IsAllowed(this, this)) @@ -792,7 +883,7 @@ namespace Barotrauma } var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); - if (!suitableConnections.Any()) + if (suitableConnections.None()) { suitableConnections = Connections.ToList(); } @@ -877,6 +968,11 @@ namespace Barotrauma destination = Connections.First().OtherLocation(this); } var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub); + if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count) + { + mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex]; + } + mission.TimesAttempted = loadedMission.TimesAttempted; availableMissions.Add(mission); if (loadedMission.SelectedMission) { selectedMissions.Add(mission); } } @@ -978,12 +1074,22 @@ namespace Barotrauma private string RandomName(LocationType type, Random rand, IEnumerable existingLocations) { + if (!type.ForceLocationName.IsNullOrEmpty()) + { + baseName = type.ForceLocationName.Value; + return baseName; + } 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) + { + baseName = Name = name; + } + public void LoadStores(XElement locationElement) { UpdateStoreIdentifiers(); @@ -1068,13 +1174,21 @@ namespace Barotrauma public int GetAdjustedMechanicalCost(int cost) { - float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); - return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); + float discount = 0.0f; + if (Reputation != null) + { + discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); + } + return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); } public int GetAdjustedHealCost(int cost) { - float discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + float discount = 0.0f; + if (Reputation != null) + { + discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + } return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); } @@ -1258,32 +1372,6 @@ namespace Barotrauma } } - public float GetStoreReputationModifier(bool buying) - { - if (buying) - { - if (Reputation.Value > 0.0f) - { - return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); - } - else - { - return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); - } - } - else - { - if (Reputation.Value > 0.0f) - { - return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); - } - else - { - return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); - } - } - } - public static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -1314,14 +1402,14 @@ namespace Barotrauma { return LevelData != null && LevelData.OutpostGenerationParamsExist && - LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType)); + LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType)); } - public void Reset() + public void Reset(CampaignMode campaign) { if (Type != OriginalType) { - ChangeType(OriginalType); + ChangeType(campaign, OriginalType); PendingLocationTypeChange = null; } CreateStores(force: true); @@ -1345,6 +1433,16 @@ namespace Barotrauma new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); + + if (Faction != null) + { + locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier)); + } + if (SecondaryFaction != null) + { + locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier)); + } + LevelData.Save(locationElement); for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -1403,6 +1501,7 @@ namespace Barotrauma { var storeElement = new XElement("store", new XAttribute("identifier", store.Identifier.Value), + new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction), new XAttribute("balance", store.Balance), new XAttribute("pricemodifier", store.PriceModifier)); foreach (PurchasedItem item in store.Stock) @@ -1442,10 +1541,13 @@ namespace Barotrauma foreach (Mission mission in missions) { var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this); - var i = map.Locations.IndexOf(location); + var destinationIndex = map.Locations.IndexOf(location); + var originIndex = map.Locations.IndexOf(mission.OriginLocation); missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), - new XAttribute("destinationindex", i), + new XAttribute("destinationindex", destinationIndex), + new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted), + new XAttribute("origin", originIndex), new XAttribute("selected", selectedMissions.Contains(mission)))); } locationElement.Add(missionsElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 2a97aa299..f2211a472 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -13,8 +13,8 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private readonly List names; - private readonly List portraits = new List(); + private readonly ImmutableArray names; + private readonly ImmutableArray portraits; // private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; @@ -26,6 +26,8 @@ namespace Barotrauma public readonly LocalizedString Name; public readonly LocalizedString Description; + public readonly LocalizedString ForceLocationName; + public readonly float BeaconStationChance; public readonly CharacterTeamType OutpostTeam; @@ -39,7 +41,12 @@ namespace Barotrauma public bool IsEnterable { get; private set; } - public bool UseInMainMenu + /// + /// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone + /// + public bool AllowInRandomLevels { get; private set; } + + public bool UsePortraitInRandomLoadingScreens { get; private set; @@ -96,6 +103,8 @@ namespace Barotrauma public int DailySpecialsCount { get; } = 1; public int RequestedGoodsCount { get; } = 1; + public readonly bool ShowSonarMarker = true; + public override string ToString() { return $"LocationType (" + Identifier + ")"; @@ -108,9 +117,12 @@ namespace Barotrauma BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); + UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); + + ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty()).ToImmutableArray(); @@ -126,23 +138,31 @@ namespace Barotrauma string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); - string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); - names = new List(); - foreach (string rawPath in rawNamePaths) + if (element.GetAttribute("name") != null) { - 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); - } + ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty)); } - if (!names.Any()) + else { - names.Add("ERROR: No names found"); + string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); + var names = new List(); + foreach (string rawPath in rawNamePaths) + { + 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); + } + } + if (!names.Any()) + { + names.Add("ERROR: No names found"); + } + this.names = names.ToImmutableArray(); } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -172,7 +192,7 @@ namespace Barotrauma } MinCountPerZone[zoneIndex] = minCount; } - + var portraits = new List(); var hireableJobs = new List<(Identifier, float)>(); foreach (var subElement in element.Elements()) { @@ -213,6 +233,7 @@ namespace Barotrauma break; } } + this.portraits = portraits.ToImmutableArray(); this.hireableJobs = hireableJobs.ToImmutableArray(); } @@ -229,10 +250,10 @@ namespace Barotrauma return null; } - public Sprite GetPortrait(int portraitId) + public Sprite GetPortrait(int randomSeed) { - if (portraits.Count == 0) { return null; } - return portraits[Math.Abs(portraitId) % portraits.Count]; + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } public string GetRandomName(Random rand, IEnumerable existingLocations) @@ -245,17 +266,34 @@ namespace Barotrauma return unusedNames[rand.Next() % unusedNames.Count]; } } - return names[rand.Next() % names.Count]; + return names[rand.Next() % names.Length]; } - public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) + public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func predicate = null) { Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); LocationType[] allowedLocationTypes = - Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)) + Prefabs.Where(lt => + (predicate == null || predicate(lt)) && IsValid(lt)) .OrderBy(p => p.UintIdentifier).ToArray(); + bool IsValid(LocationType lt) + { + if (requireOutpost && !lt.HasOutpost) { return false; } + if (zone.HasValue) + { + if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; } + } + //if zone is not defined, this is a "random" (non-campaign) level + //-> don't choose location types that aren't allowed in those + else if (!lt.AllowInRandomLevels) + { + return false; + } + return true; + } + if (allowedLocationTypes.Length == 0) { DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 0639ceb6f..8d9e03673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; +using static Barotrauma.LocationTypeChange; namespace Barotrauma { @@ -58,7 +58,7 @@ namespace Barotrauma public Requirement(XElement element, LocationTypeChange change) { RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); - RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); + RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false); @@ -91,37 +91,30 @@ namespace Barotrauma } } + public bool AnyWithinDistance(Location startLocation, int distance) + { + return Map.LocationOrConnectionWithinDistance( + startLocation, + maxDistance: distance, + criteria: MatchesLocation, + connectionCriteria: MatchesConnection); + } + public bool MatchesLocation(Location location) { return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated(); } - public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet checkedLocations = null) + public bool MatchesConnection(LocationConnection connection) { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && MatchesLocation(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) + if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) { - if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) - { - return true; - } - if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) - { - return true; - } - - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; } - } + return true; + } + if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) + { + return true; } - return false; } } @@ -141,24 +134,25 @@ namespace Barotrauma private readonly bool requireChangeMessages; private readonly string messageTag; - private ImmutableArray? messages = null; - public IReadOnlyList Messages - { - get - { - if (!messages.HasValue) - { - messages = TextManager.GetAll(messageTag).ToImmutableArray(); - if (messages.Value.None()) - { - if (requireChangeMessages) - { - DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); - } - } - } - return messages.Value; + public IReadOnlyList GetMessages(Faction faction) + { + if (faction != null && TextManager.ContainsTag(messageTag + "." + faction.Prefab.Identifier)) + { + return TextManager.GetAll(messageTag + "." + faction.Prefab.Identifier).ToImmutableArray(); + } + + if (TextManager.ContainsTag(messageTag)) + { + return TextManager.GetAll(messageTag).ToImmutableArray(); + } + else + { + if (requireChangeMessages) + { + DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); + } + return Enumerable.Empty().ToImmutableArray(); } } @@ -226,8 +220,9 @@ namespace Barotrauma if (location.LocationTypeChangeCooldown > 0) { return 0.0f; } if (location.IsGateBetweenBiomes) { return 0.0f; } - if (DisallowedAdjacentLocations.Any() && - AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + if (DisallowedAdjacentLocations.Any() && + Map.LocationOrConnectionWithinDistance(location, DisallowedProximity, + (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) { return 0.0f; } @@ -246,7 +241,6 @@ namespace Barotrauma probability *= requirement.Probability; } } - if (location.ProximityTimer.ContainsKey(requirement)) { if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) @@ -265,25 +259,5 @@ namespace Barotrauma return probability; } - - private bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) - { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && predicate(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) - { - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; } - } - } - - return false; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index df2c0679c..e361bd38b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -39,7 +39,8 @@ namespace Barotrauma /// public readonly NamedEvent OnLocationChanged = new NamedEvent(); - public Location EndLocation { get; private set; } + private List endLocations = new List(); + public IReadOnlyList EndLocations { get { return endLocations; } } public Location StartLocation { get; private set; } @@ -69,13 +70,13 @@ namespace Barotrauma public List Locations { get; private set; } private readonly List locationsDiscovered = new List(); - private readonly List outpostsVisited = new List(); + private readonly List locationsVisited = new List(); public List Connections { get; private set; } public Radiation Radiation; - private bool wasLocationDiscoveryOrderTracked = true; + private bool trackedLocationDiscoveryAndVisitOrder = true; public Map(CampaignSettings settings) { @@ -117,7 +118,7 @@ namespace Barotrauma Locations.Add(null); } lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase); - Locations[i] = new Location(subElement); + Locations[i] = new Location(campaign, subElement); break; case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement) @@ -127,11 +128,6 @@ namespace Barotrauma break; } } - System.Diagnostics.Debug.Assert(!Locations.Contains(null)); - for (int i = 0; i < Locations.Count; i++) - { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); - } List connectionElements = new List(); foreach (var subElement in element.Elements()) @@ -187,23 +183,72 @@ namespace Barotrauma } } } - int endLocationindex = element.GetAttributeInt("endlocation", -1); - if (endLocationindex >= 0 && endLocationindex < Locations.Count) + + if (element.GetAttribute("endlocation") != null) { - EndLocation = Locations[endLocationindex]; + //backwards compatibility + int endLocationIndex = element.GetAttributeInt("endlocation", -1); + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) + { + endLocations.Add(Locations[endLocationIndex]); + } + else + { + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count})."); + } } else { - DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationindex}, location count: {Locations.Count})."); - foreach (Location location in Locations) + int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty()); + foreach (int endLocationIndex in endLocationindices) { - if (EndLocation == null || location.MapPosition.X > EndLocation.MapPosition.X) + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) { - EndLocation = location; + endLocations.Add(Locations[endLocationIndex]); + } + else + { + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count})."); } } } + if (!endLocations.Any()) + { + DebugConsole.AddWarning($"Error while loading the map. No end location(s) found. Choosing the rightmost location as the end location..."); + Location endLocation = null; + foreach (Location location in Locations) + { + if (endLocation == null || location.MapPosition.X > endLocation.MapPosition.X) + { + endLocation = location; + } + } + endLocations.Add(endLocation); + } + + System.Diagnostics.Debug.Assert(endLocations.First().Biome != null, "End location biome was null."); + System.Diagnostics.Debug.Assert(endLocations.First().Biome.IsEndBiome, "The biome of the end location isn't the end biome."); + + //backwards compatibility (or support for loading maps created with mods that modify the end biome setup): + //if there's too few end locations, create more + int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count; + + Location firstEndLocation = EndLocations[0]; + for (int i = 0; i < missingOutpostCount; i++) + { + Vector2 mapPos = new Vector2( + MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)), + Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble())); + var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations) + { + Biome = endLocations.First().Biome + }; + newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f); + Locations.Add(newEndLocation); + endLocations.Add(newEndLocation); + } + //backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds)) { @@ -214,6 +259,17 @@ namespace Barotrauma } } + foreach (var endLocation in EndLocations) + { + if (endLocation.Type?.ForceLocationName != null && + !endLocation.Type.ForceLocationName.IsNullOrEmpty()) + { + endLocation.ForceName(endLocation.Type.ForceLocationName.Value); + } + } + + AssignEndLocationLevelData(); + //backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml) float maxX = Locations.Select(l => l.MapPosition.X).Max(); if (maxX > Width) { Width = (int)(maxX + 10); } @@ -231,18 +287,13 @@ namespace Barotrauma Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); - Generate(campaign.Settings); + Generate(campaign); if (Locations.Count == 0) { throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}"); } - for (int i = 0; i < Locations.Count; i++) - { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); - } - foreach (Location location in Locations) { if (location.Type.Identifier != "outpost") { continue; } @@ -263,6 +314,20 @@ namespace Barotrauma if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) { CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + StartLocation.SecondaryFaction = null; + var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); + if (startOutpostFaction != null) + { + StartLocation.Faction = startOutpostFaction; + foreach (var connection in StartLocation.Connections) + { + var otherLocation = connection.OtherLocation(StartLocation); + if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + otherLocation.Faction = startOutpostFaction; + } + } + } } } @@ -273,7 +338,7 @@ namespace Barotrauma { if (StartLocation != null) { - StartLocation.LevelData = new LevelData(StartLocation, 0); + StartLocation.LevelData = new LevelData(StartLocation, this, 0); } //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy @@ -289,7 +354,7 @@ namespace Barotrauma if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost)) { - CurrentLocation.ChangeType(tutorialOutpost); + CurrentLocation.ChangeType(campaign, tutorialOutpost); } Discover(CurrentLocation); Visit(CurrentLocation); @@ -307,7 +372,7 @@ namespace Barotrauma #region Generation - private void Generate(CampaignSettings settings) + private void Generate(CampaignMode campaign) { Connections.Clear(); Locations.Clear(); @@ -525,12 +590,14 @@ namespace Barotrauma connectionsBetweenZones[zone1].Add(connection); } } - else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1]) + else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] && + connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1]))) { connectionsBetweenZones[zone1].Add(connection); } } + var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList(); for (int i = Connections.Count - 1; i >= 0; i--) { int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X); @@ -538,9 +605,9 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; } - if (generationParams.GateCount[Math.Min(zone1, zone2)] == 0) { continue; } - - if (!connectionsBetweenZones[Math.Min(zone1, zone2)].Contains(Connections[i])) + int leftZone = Math.Min(zone1, zone2); + if (generationParams.GateCount[leftZone] == 0) { continue; } + if (!connectionsBetweenZones[leftZone].Contains(Connections[i])) { Connections.RemoveAt(i); } @@ -552,11 +619,18 @@ namespace Barotrauma Connections[i].Locations[1]; if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + leftMostLocation.ChangeType( + campaign, + LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), createStores: false); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; + + if (leftMostLocation.Type.HasOutpost && campaign != null && gateFactions.Any()) + { + leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(Connections[i]) % gateFactions.Count]; + } } } @@ -624,21 +698,27 @@ namespace Barotrauma } } - CreateEndLocation(); - foreach (Location location in Locations) { - location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome)); + if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } location.CreateStores(force: true); } + foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } + CreateEndLocation(campaign); + float CalculateDifficulty(float mapPosition, Biome biome) { - float settingsFactor = settings.LevelDifficultyMultiplier; + float settingsFactor = campaign.Settings.LevelDifficultyMultiplier; float minDifficulty = 0; float maxDifficulty = 100; float difficulty = mapPosition / Width * 100; @@ -707,18 +787,18 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null)); } - private void CreateEndLocation() + private void CreateEndLocation(CampaignMode campaign) { float zoneWidth = Width / generationParams.DifficultyZones; - Vector2 endPos = new Vector2(Width - zoneWidth / 2, Height / 2); + Vector2 endPos = new Vector2(Width - zoneWidth * 0.7f, Height / 2); float closestDist = float.MaxValue; - EndLocation = Locations.First(); + var endLocation = Locations.First(); foreach (Location location in Locations) { float dist = Vector2.DistanceSquared(endPos, location.MapPosition); if (location.Biome.IsEndBiome && dist < closestDist) { - EndLocation = location; + endLocation = location; closestDist = dist; } } @@ -732,17 +812,39 @@ namespace Barotrauma } } - if (EndLocation == null || previousToEndLocation == null) { return; } + if (endLocation == null || previousToEndLocation == null) { return; } + + endLocations = new List() { endLocation }; + if (endLocation.Biome.EndBiomeLocationCount > 1) + { + FindConnectedEndLocations(endLocation); + + void FindConnectedEndLocations(Location currLocation) + { + if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; } + foreach (var connection in currLocation.Connections) + { + if (connection.Biome != endLocation.Biome) { continue; } + var otherLocation = connection.OtherLocation(currLocation); + if (otherLocation != null && !endLocations.Contains(otherLocation)) + { + if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; } + endLocations.Add(otherLocation); + FindConnectedEndLocations(otherLocation); + } + } + } + } if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) { - previousToEndLocation.ChangeType(locationType, createStores: false); + previousToEndLocation.ChangeType(campaign, locationType, createStores: false); } //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) { - if (Locations[i].Biome.IsEndBiome && Locations[i] != EndLocation) + if (Locations[i].Biome.IsEndBiome) { for (int j = Locations[i].Connections.Count - 1; j >= 0; j--) { @@ -753,7 +855,10 @@ namespace Barotrauma otherLocation?.Connections.Remove(connection); Connections.Remove(connection); } - Locations.RemoveAt(i); + if (!endLocations.Contains(Locations[i])) + { + Locations.RemoveAt(i); + } } } @@ -770,22 +875,39 @@ namespace Barotrauma } var newConnection = new LocationConnection(previousToEndLocation, connectTo) { - Biome = EndLocation.Biome, + Biome = endLocation.Biome, Difficulty = 100.0f }; + newConnection.LevelData = new LevelData(newConnection); Connections.Add(newConnection); previousToEndLocation.Connections.Add(newConnection); connectTo.Connections.Add(newConnection); } - var endConnection = new LocationConnection(previousToEndLocation, EndLocation) + var endConnection = new LocationConnection(previousToEndLocation, endLocation) { - Biome = EndLocation.Biome, + Biome = endLocation.Biome, Difficulty = 100.0f }; + endConnection.LevelData = new LevelData(endConnection); Connections.Add(endConnection); previousToEndLocation.Connections.Add(endConnection); - EndLocation.Connections.Add(endConnection); + endLocation.Connections.Add(endConnection); + + AssignEndLocationLevelData(); + } + + private void AssignEndLocationLevelData() + { + for (int i = 0; i < endLocations.Count; i++) + { + endLocations[i].LevelData.ReassignGenerationParams(Seed); + var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i); + if (outpostParams != null) + { + endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams; + } + } } private void ExpandBiomes(List seeds) @@ -817,19 +939,48 @@ namespace Barotrauma public void MoveToNextLocation() { + if (SelectedLocation == null && Level.Loaded?.EndLocation != null) + { + //force the location at the end of the level to be selected, even if it's been deselect on the map + //(e.g. due to returning to an empty location the beginning of the level during the round) + SelectLocation(Level.Loaded.EndLocation); + } if (SelectedConnection == null) { - DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace.CleanupStackTrace()); - return; + if (!endLocations.Contains(CurrentLocation)) + { + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } } if (SelectedLocation == null) { - DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace.CleanupStackTrace()); - return; + if (endLocations.Contains(CurrentLocation)) + { + int currentEndLocationIndex = endLocations.IndexOf(CurrentLocation); + if (currentEndLocationIndex < endLocations.Count - 1) + { + //more end locations to go, progress to the next one + SelectedLocation = endLocations[currentEndLocationIndex + 1]; + } + else + { + //at the last end location, end of campaign + SelectedLocation = StartLocation; + } + } + else + { + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } } Location prevLocation = CurrentLocation; - SelectedConnection.Passed = true; + if (SelectedConnection != null) + { + SelectedConnection.Passed = true; + } CurrentLocation = SelectedLocation; Discover(CurrentLocation); @@ -898,12 +1049,24 @@ namespace Barotrauma Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); - SelectedConnection = - Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? - Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + if (currentDisplayLocation == SelectedLocation) + { + SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } + else + { + SelectedConnection = + Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? + Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } if (SelectedConnection?.Locked ?? false) { - DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); + string errorMsg = + $"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." + + $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n" + + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); } if (prevSelected != SelectedLocation) { @@ -979,7 +1142,7 @@ namespace Barotrauma } } - public void ProgressWorld(CampaignMode.TransitionType transitionType, float roundDuration) + public void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration) { //one step per 10 minutes of play time int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f)); @@ -992,7 +1155,7 @@ namespace Barotrauma steps = Math.Min(steps, 5); for (int i = 0; i < steps; i++) { - ProgressWorld(); + ProgressWorld(campaign); } // always update specials every step @@ -1008,7 +1171,7 @@ namespace Barotrauma Radiation?.OnStep(steps); } - private void ProgressWorld() + private void ProgressWorld(CampaignMode campaign) { foreach (Location location in Locations) { @@ -1036,14 +1199,14 @@ namespace Barotrauma if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; } - if (!ProgressLocationTypeChanges(location) && location.Discovered) + if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered) { location.UpdateStores(); } } } - private bool ProgressLocationTypeChanges(Location location) + private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location) { location.TimeSinceLastTypeChange++; location.LocationTypeChangeCooldown--; @@ -1063,7 +1226,7 @@ namespace Barotrauma location.PendingLocationTypeChange.Value.parentMission); if (location.PendingLocationTypeChange.Value.delay <= 0) { - return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); + return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange); } } } @@ -1096,7 +1259,7 @@ namespace Barotrauma } else { - return ChangeLocationType(location, selectedTypeChange); + return ChangeLocationType(campaign, location, selectedTypeChange); } return false; } @@ -1121,52 +1284,7 @@ namespace Barotrauma return false; } - public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) - { - if (startingLocation.Type.HasOutpost) - { - endingLocation = startingLocation; - return 0; - } - - int iterations = 0; - int distance = 0; - endingLocation = null; - - List testedLocations = new List(); - List locationsToTest = new List { startingLocation }; - - while (endingLocation == null && iterations < 100) - { - List nextTestingBatch = new List(); - for (int i = 0; i < locationsToTest.Count; i++) - { - Location testLocation = locationsToTest[i]; - for (int j = 0; j < testLocation.Connections.Count; j++) - { - Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation); - if (potentialOutpost.Type.HasOutpost) - { - distance = iterations + 1; - endingLocation = potentialOutpost; - } - else if (!testedLocations.Contains(potentialOutpost)) - { - nextTestingBatch.Add(potentialOutpost); - } - } - - testedLocations.Add(testLocation); - } - - locationsToTest = nextTestingBatch; - iterations++; - } - - return distance; - } - - private bool ChangeLocationType(Location location, LocationTypeChange change) + private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { string prevName = location.Name; @@ -1176,12 +1294,14 @@ namespace Barotrauma return false; } + if (location.LocationTypeChangesBlocked) { return false; } + if (newType.OutpostTeam != location.Type.OutpostTeam || newType.HasOutpost != location.Type.HasOutpost) { location.ClearMissions(); } - location.ChangeType(newType); + location.ChangeType(campaign, newType); ChangeLocationTypeProjSpecific(location, prevName, change); foreach (var requirement in change.Requirements) { @@ -1193,6 +1313,50 @@ namespace Barotrauma return true; } + public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance; + } + + /// + /// Get the shortest distance from the start location to another location that satisfies the specified criteria. + /// + /// The distance to a matching location, or int.MaxValue if none are found. + public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + int distance = 0; + var locationsToTest = new List() { startLocation }; + var nextBatchToTest = new HashSet(); + var checkedLocations = new HashSet(); + while (locationsToTest.Any()) + { + foreach (var location in locationsToTest) + { + checkedLocations.Add(location); + if (criteria(location)) { return distance; } + foreach (var connection in location.Connections) + { + if (connectionCriteria != null && connectionCriteria(connection)) + { + return distance; + } + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + nextBatchToTest.Add(otherLocation); + } + } + if (distance > maxDistance) { return int.MaxValue; } + } + distance++; + locationsToTest.Clear(); + locationsToTest.AddRange(nextBatchToTest); + nextBatchToTest.Clear(); + } + return int.MaxValue; + } + + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); partial void ClearAnimQueue(); @@ -1211,29 +1375,38 @@ namespace Barotrauma public void Visit(Location location) { if (location is null) { return; } - if (!location.HasOutpost()) { return; } - if (outpostsVisited.Contains(location)) { return; } - outpostsVisited.Add(location); + if (locationsVisited.Contains(location)) { return; } + locationsVisited.Add(location); + RemoveFogOfWarProjSpecific(location); } public void ClearLocationHistory() { locationsDiscovered.Clear(); - outpostsVisited.Clear(); + locationsVisited.Clear(); } public int? GetDiscoveryIndex(Location location) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } return locationsDiscovered.IndexOf(location); } - public int? GetVisitIndex(Location location) + public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } - return outpostsVisited.IndexOf(location); + int index = locationsVisited.IndexOf(location); + if (includeLocationsWithoutOutpost) { return index; } + int noOutpostLocations = 0; + for (int i = 0; i < index; i++) + { + if (locationsVisited[i] is not Location l) { continue; } + if (l.HasOutpost()) { continue; } + noOutpostLocations++; + } + return index - noOutpostLocations; } public bool IsDiscovered(Location location) @@ -1242,13 +1415,21 @@ namespace Barotrauma return locationsDiscovered.Contains(location); } + public bool IsVisited(Location location) + { + if (location is null) { return false; } + return locationsVisited.Contains(location); + } + + partial void RemoveFogOfWarProjSpecific(Location location); + /// /// Load a previously saved map from an xml element /// public static Map Load(CampaignMode campaign, XElement element) { Map map = new Map(campaign, element); - map.LoadState(element, false); + map.LoadState(campaign, element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; #endif @@ -1258,12 +1439,12 @@ namespace Barotrauma /// /// Load the state of an existing map from xml (current state of locations, where the crew is now, etc). /// - public void LoadState(XElement element, bool showNotifications) + public void LoadState(CampaignMode campaign, XElement element, bool showNotifications) { ClearAnimQueue(); SetLocation(element.GetAttributeInt("currentlocation", 0)); - if (!Version.TryParse(element.GetAttributeString("version", ""), out _)) + if (!Version.TryParse(element.GetAttributeString("version", ""), out Version version)) { DebugConsole.ThrowError("Incompatible map save file, loading the game failed."); return; @@ -1275,7 +1456,13 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "location": - Location location = Locations[subElement.GetAttributeInt("i", 0)]; + int locationIndex = subElement.GetAttributeInt("i", -1); + if (locationIndex < 0 || locationIndex >= Locations.Count) + { + DebugConsole.AddWarning($"Error while loading the campaign map: location index out of bounds ({locationIndex})"); + continue; + } + Location location = Locations[locationIndex]; location.ProximityTimer.Clear(); for (int i = 0; i < location.Type.CanChangeTo.Count; i++) { @@ -1286,18 +1473,20 @@ namespace Barotrauma } location.LoadLocationTypeChange(subElement); - // Backwards compatibility + // Backwards compatibility: if the discovery status is defined in the location element, + // the game was saved using when the discovery order still wasn't being tracked if (subElement.GetAttributeBool("discovered", false)) { Discover(location); - wasLocationDiscoveryOrderTracked = false; + Visit(location); + trackedLocationDiscoveryAndVisitOrder = false; } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); - location.ChangeType(newLocationType); + location.ChangeType(campaign, newLocationType); if (showNotifications && prevLocationType != location.Type) { var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); @@ -1308,45 +1497,72 @@ namespace Barotrauma } } + var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); + location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + + var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); + location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); + location.LoadStores(subElement); location.LoadMissions(subElement); break; case "connection": - int connectionIndex = subElement.GetAttributeInt("i", 0); + //the index wasn't saved previously, skip if that's the case + if (subElement.Attribute("i") == null) { continue; } + + int connectionIndex = subElement.GetAttributeInt("i", -1); + if (connectionIndex < 0 || connectionIndex >= Connections.Count) + { + DebugConsole.AddWarning($"Error while loading the campaign map: connection index out of bounds ({connectionIndex})"); + continue; + } Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false); - Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); + Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); break; case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; case "discovered": + bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false); foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Discover(l); + if (GetLocation(childElement) is Location l) + { + Discover(l); + if (!trackedVisitedEmptyLocations) + { + if (!l.HasOutpost()) + { + Visit(l); + } + trackedLocationDiscoveryAndVisitOrder = false; + } + } } break; case "visited": foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Visit(l); + if (GetLocation(childElement) is Location l) + { + Visit(l); + } } break; } + + Location GetLocation(XElement element) + { + int index = element.GetAttributeInt("i", -1); + if (index < 0) { return null; } + return Locations[index]; + } } void Discover(Location location) { this.Discover(location, checkTalents: false); -#if CLIENT - RemoveFogOfWar(location); -#endif if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = location; @@ -1358,6 +1574,24 @@ namespace Barotrauma location?.InstantiateLoadedMissions(this); } + //backwards compatibility: + //if the save is from a version prior to the addition of faction-specific outposts, assign factions + if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null)) + { + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); + foreach (Location location in Locations) + { + if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + location.Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + if (location != StartLocation) + { + location.SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } + } + } + } + int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1); if (currentLocationConnection >= 0) { @@ -1396,7 +1630,7 @@ namespace Barotrauma mapElement.Add(new XAttribute("height", Height)); mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex)); mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation))); - mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation))); + mapElement.Add(new XAttribute("endlocations", string.Join(',', EndLocations.Select(e => Locations.IndexOf(e))))); mapElement.Add(new XAttribute("seed", Seed)); for (int i = 0; i < Locations.Count; i++) @@ -1415,6 +1649,7 @@ namespace Barotrauma new XAttribute("locked", connection.Locked), new XAttribute("difficulty", connection.Difficulty), new XAttribute("biome", connection.Biome.Identifier), + new XAttribute("i", i), new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1]))); connection.LevelData.Save(connectionElement); mapElement.Add(connectionElement); @@ -1427,7 +1662,8 @@ namespace Barotrauma if (locationsDiscovered.Any()) { - var discoveryElement = new XElement("discovered"); + var discoveryElement = new XElement("discovered", + new XAttribute("trackedvisitedemptylocations", true)); foreach (Location location in locationsDiscovered) { int index = Locations.IndexOf(location); @@ -1437,10 +1673,10 @@ namespace Barotrauma mapElement.Add(discoveryElement); } - if (outpostsVisited.Any()) + if (locationsVisited.Any()) { var visitElement = new XElement("visited"); - foreach (Location location in outpostsVisited) + foreach (Location location in locationsVisited) { int index = Locations.IndexOf(location); var locationElement = new XElement("location", new XAttribute("i", index)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index e8489c131..dd9c99176 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -19,9 +19,6 @@ namespace Barotrauma protected List linkedToID; public List unresolvedLinkedToID; - private const int GapUpdateInterval = 4; - private static int gapUpdateTimer; - /// /// List of upgrades this item has /// @@ -59,7 +56,24 @@ namespace Barotrauma //the position and dimensions of the entity protected Rectangle rect; - public bool ExternalHighlight = false; + protected static readonly HashSet highlightedEntities = new HashSet(); + + public static IEnumerable HighlightedEntities => highlightedEntities; + + + private bool externalHighlight = false; + public bool ExternalHighlight + { + get { return externalHighlight; } + set + { + if (value != externalHighlight) + { + externalHighlight = value; + CheckIsHighlighted(); + } + } + } //is the mouse inside the rect private bool isHighlighted; @@ -67,7 +81,14 @@ namespace Barotrauma public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } - set { isHighlighted = value; } + set + { + if (value != IsHighlighted) + { + isHighlighted = value; + CheckIsHighlighted(); + } + } } public virtual Rectangle Rect @@ -158,7 +179,7 @@ namespace Barotrauma { if (!float.IsNaN(value)) { - _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); + _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999999f); if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } SpriteDepthOverrideIsSet = true; } @@ -362,6 +383,31 @@ namespace Barotrauma return true; } + protected virtual void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + + private static readonly List tempHighlightedEntities = new List(); + public static void ClearHighlightedEntities() + { + highlightedEntities.RemoveWhere(e => e.Removed); + tempHighlightedEntities.Clear(); + tempHighlightedEntities.AddRange(highlightedEntities); + foreach (var entity in tempHighlightedEntities) + { + entity.IsHighlighted = false; + } + } + + public abstract MapEntity Clone(); public static List Clone(List entitiesToClone) @@ -458,7 +504,7 @@ namespace Barotrauma } (clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire); - cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false); + cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], n, addNode: false); } if ((cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) @@ -579,14 +625,9 @@ namespace Barotrauma //the water/air will always tend to flow through the first gap in the list, //which may lead to weird behavior like water draining down only through //one gap in a room even if there are several - gapUpdateTimer++; - if (gapUpdateTimer >= GapUpdateInterval) + foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) { - foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) - { - gap.Update(deltaTime * GapUpdateInterval, cam); - } - gapUpdateTimer = 0; + gap.Update(deltaTime, cam); } #if CLIENT @@ -649,6 +690,9 @@ namespace Barotrauma List entities = new List(); foreach (var element in parentElement.Elements()) { +#if CLIENT + GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested(); +#endif string typeName = element.Name.ToString(); Type t; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs index 7d9f33be6..feec0ea3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs @@ -17,6 +17,9 @@ namespace Barotrauma [Serialize(100.0f, IsPropertySaveable.Yes), Editable] public float MaxLevelDifficulty { get; set; } + [Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable] + public Level.PlacementType Placement { get; set; } + public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 965965805..8be567543 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -1,10 +1,6 @@ #nullable enable -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -12,23 +8,23 @@ namespace Barotrauma { public readonly static PrefabCollection Sets = new PrefabCollection(); - private readonly ImmutableArray Humans; - private bool Disposed { get; set; } - public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file, Identifier)).ToImmutableArray(); } - public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier) + public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier, bool logError = true) { HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); if (prefab == null) { - DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); + if (logError) + { + DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); + } return null; } return prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index f91f5e1a6..35a758882 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -23,77 +23,106 @@ namespace Barotrauma get { return allowedLocationTypes; } } - [Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)] + + [Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)] + public int ForceToEndLocationIndex + { + get; + set; + } + + + [Serialize(10, IsPropertySaveable.Yes, description: "Total number of modules in the outpost."), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount { get; set; } - [Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the generator append generic (module flag \"none\") modules to the outpost to reach the total module count."), Editable] + public bool AppendToReachTotalModuleCount + { + get; + set; + } + + [Serialize(200.0f, IsPropertySaveable.Yes, description: "Minimum length of the hallways between modules. If 0, the generator will place the modules directly against each other assuming it can be done without making any modules overlap."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float MinHallwayLength { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be destructible, regardless if damaging outposts is allowed by the server?"), Editable] public bool AlwaysDestructible { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be rewireable, regardless if rewiring is allowed by the server?"), Editable] public bool AlwaysRewireable { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should stealing from this outpost be always allowed?"), Editable] public bool AllowStealing { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the crew spawn inside the outpost (if not, they'll spawn in the submarine)."), Editable] public bool SpawnCrewInsideOutpost { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should doors at the edges of an outpost module that didn't get connected to another module be locked?"), Editable] public bool LockUnusedDoors { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should gaps at the edges of an outpost module that didn't get connected to another module be removed?"), Editable] public bool RemoveUnusedGaps { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the whole outpost render behind submarines? Only set this to true if the submarine is intended to go inside the outpost."), Editable] + public bool DrawBehindSubs + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinWaterPercentage { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Maximum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MaxWaterPercentage { get; set; } - [Serialize("", IsPropertySaveable.Yes), Editable] + public LevelData.LevelType? LevelType + { + get; + set; + } + + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable] public string ReplaceInRadiation { get; set; } public ContentPath OutpostFilePath { get; set; } @@ -104,17 +133,21 @@ namespace Barotrauma public int Count; public int Order; + public Identifier RequiredFaction; + public ModuleCount(ContentXElement element) { Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", "")); Count = element.GetAttributeInt("count", 0); Order = element.GetAttributeInt("order", 0); + RequiredFaction = element.GetAttributeIdentifier("requiredfaction", Identifier.Empty); } public ModuleCount(Identifier id, int count) { Identifier = id; Count = count; + RequiredFaction = Identifier.Empty; } } @@ -132,16 +165,20 @@ namespace Barotrauma private readonly HumanPrefab humanPrefab = null; private readonly Identifier setIdentifier = Identifier.Empty; private readonly Identifier npcIdentifier = Identifier.Empty; + + public readonly Identifier FactionIdentifier = Identifier.Empty; - public Entry(HumanPrefab humanPrefab) + public Entry(HumanPrefab humanPrefab, Identifier factionIdentifier) { this.humanPrefab = humanPrefab; + this.FactionIdentifier = factionIdentifier; } - public Entry(Identifier setIdentifier, Identifier npcIdentifier) + public Entry(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier) { this.setIdentifier = setIdentifier; this.npcIdentifier = npcIdentifier; + this.FactionIdentifier = factionIdentifier; } public HumanPrefab HumanPrefab @@ -150,29 +187,41 @@ namespace Barotrauma private readonly List entries = new List(); - public void Add(HumanPrefab humanPrefab) - => entries.Add(new Entry(humanPrefab)); + public void Add(HumanPrefab humanPrefab, Identifier factionIdentifier) + => entries.Add(new Entry(humanPrefab, factionIdentifier)); - public void Add(Identifier setIdentifier, Identifier npcIdentifier) - => entries.Add(new Entry(setIdentifier, npcIdentifier)); + public void Add(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier) + => entries.Add(new Entry(setIdentifier, npcIdentifier, factionIdentifier)); public IEnumerator GetEnumerator() { foreach (var entry in entries) { + if (entry == null) { continue; } yield return entry.HumanPrefab; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerable GetByFaction(IEnumerable factions) + { + foreach (var entry in entries) + { + if (entry.FactionIdentifier == Identifier.Empty || factions.Any(f => f.Identifier == entry.FactionIdentifier)) + { + yield return entry.HumanPrefab; + } + } + } + public int Count => entries.Count; public HumanPrefab this[int index] => entries[index].HumanPrefab; } - private readonly ImmutableArray> humanPrefabCollections; + private readonly ImmutableArray humanPrefabCollections; public Dictionary SerializableProperties { get; private set; } @@ -184,9 +233,23 @@ namespace Barotrauma Name = element.GetAttributeString("name", Identifier.Value); allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + if (element.GetAttribute("leveltype") != null) + { + string levelTypeStr = element.GetAttributeString("leveltype", ""); + if (Enum.TryParse(levelTypeStr, out LevelData.LevelType parsedLevelType)) + { + LevelType = parsedLevelType; + } + else + { + DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + } + } + OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath)); - var humanPrefabCollections = new List>(); + var humanPrefabCollections = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -199,14 +262,14 @@ namespace Barotrauma foreach (var npcElement in subElement.Elements()) { Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty); - + Identifier faction = npcElement.GetAttributeIdentifier("faction", Identifier.Empty); if (from != Identifier.Empty) { - newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty)); + newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty), faction); } else { - newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from)); + newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from), faction); } } humanPrefabCollections.Add(newCollection); @@ -254,10 +317,12 @@ namespace Barotrauma } } - public IReadOnlyList GetHumanPrefabs(Rand.RandSync randSync) + public IReadOnlyList GetHumanPrefabs(IEnumerable factions, Rand.RandSync randSync) { if (!humanPrefabCollections.Any()) { return Array.Empty(); } - return humanPrefabCollections.GetRandom(randSync); + + var collection = humanPrefabCollections.GetRandom(randSync); + return collection.GetByFaction(factions).ToImmutableList(); } public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType) @@ -266,7 +331,7 @@ namespace Barotrauma { foreach (var prefab in collection) { - if (prefab.CampaignInteractionType == interactionType) + if (prefab != null && prefab.CampaignInteractionType == interactionType) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 414c4c8da..c0f436f27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -150,11 +150,14 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - List pendingModuleFlags = - onlyEntrance ? - (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : - SelectModules(outpostModules, generationParams); - + List pendingModuleFlags = new List(); + if (generationParams.ModuleCounts.Any()) + { + pendingModuleFlags = onlyEntrance ? + generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() : + SelectModules(outpostModules, location, generationParams); + } + foreach (Identifier flag in pendingModuleFlags) { if (flag == "none") { continue; } @@ -246,12 +249,17 @@ namespace Barotrauma wp.FindHull(); } } + EnableFactionSpecificEntities(sub, location); return sub; } remainingTries--; } +#if DEBUG + DebugConsole.ThrowError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); +#else DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); +#endif var outpostFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -268,6 +276,7 @@ namespace Barotrauma sub = new Submarine(prebuiltOutpostInfo); sub.Info.OutpostGenerationParams = generationParams; location?.RemoveTakenItems(); + EnableFactionSpecificEntities(sub, location); return sub; List loadEntities(Submarine sub) @@ -297,7 +306,7 @@ namespace Barotrauma } idOffset = moduleEntities.Max(e => e.ID) + 1; - var wallEntities = moduleEntities.Where(e => e is Structure).Cast(); + var wallEntities = moduleEntities.Where(e => e is Structure s && s.HasBody).Cast(); var hullEntities = moduleEntities.Where(e => e is Hull).Cast(); // Tell the hulls what tags the module has, used to spawn NPCs on specific rooms @@ -306,18 +315,27 @@ namespace Barotrauma hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags); } - selectedModule.HullBounds = new Rectangle( - hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height), - hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y)); - selectedModule.HullBounds = new Rectangle( - selectedModule.HullBounds.X, selectedModule.HullBounds.Y, - selectedModule.HullBounds.Width - selectedModule.HullBounds.X, selectedModule.HullBounds.Height - selectedModule.HullBounds.Y); - selectedModule.Bounds = new Rectangle( - wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height), - wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y)); - selectedModule.Bounds = new Rectangle( - selectedModule.Bounds.X, selectedModule.Bounds.Y, - selectedModule.Bounds.Width - selectedModule.Bounds.X, selectedModule.Bounds.Height - selectedModule.Bounds.Y); + if (!hullEntities.Any()) + { + selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint()); + } + else + { + Point min = new Point(hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height)); + Point max = new Point(hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y)); + selectedModule.HullBounds = new Rectangle(min, max - min); + } + + if (!wallEntities.Any()) + { + selectedModule.Bounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint()); + } + else + { + Point min = new Point(wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height)); + Point max = new Point(wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y)); + selectedModule.Bounds = new Rectangle(min, max - min); + } if (selectedModule.PreviousModule != null) { @@ -406,6 +424,23 @@ namespace Barotrauma { LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps); } + if (generationParams.DrawBehindSubs) + { + foreach (var entity in allEntities) + { + if (entity is Structure structure) + { + //eww + structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth); +#if CLIENT + foreach (var light in structure.Lights) + { + light.IsBackground = true; + } +#endif + } + } + } AlignLadders(selectedModules, entities); PowerUpOutpost(entities.SelectMany(e => e.Value)); if (generationParams.MaxWaterPercentage > 0.0f) @@ -436,7 +471,7 @@ namespace Barotrauma /// /// Select the number and types of the modules to use in the outpost /// - private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) + private static List SelectModules(IEnumerable modules, Location location, OutpostGenerationParams generationParams) { int totalModuleCount = generationParams.TotalModuleCount; var pendingModuleFlags = new List(); @@ -447,23 +482,29 @@ namespace Barotrauma while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { availableModulesFound = false; - foreach (var moduleFlag in generationParams.ModuleCounts) + foreach (var moduleCount in generationParams.ModuleCounts) { - if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier)) + if (!moduleCount.RequiredFaction.IsEmpty && + location.Faction?.Prefab.Identifier != moduleCount.RequiredFaction && + location.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction) { continue; } - if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier))) + if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier)) { - DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found)."); + continue; + } + if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier))) + { + DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleCount.Identifier}\" found)."); continue; } availableModulesFound = true; - pendingModuleFlags.Add(moduleFlag.Identifier); + pendingModuleFlags.Add(moduleCount.Identifier); } } - pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); - while (pendingModuleFlags.Count < totalModuleCount) + pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f).Order).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); + while (pendingModuleFlags.Count < totalModuleCount && generationParams.AppendToReachTotalModuleCount) { //don't place "none" modules at the end because // a. "filler rooms" at the end of a hallway are pointless @@ -514,12 +555,8 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) - { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - + if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } + PlacedModule newModule = null; //try appending to the current module if possible if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) @@ -540,6 +577,7 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition otherGapPosition in GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) { + if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); if (newModule != null) { @@ -588,6 +626,16 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); } + + static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule) + { + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; } + } + return false; + } } /// @@ -1057,8 +1105,8 @@ namespace Barotrauma DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.PreviousModule.Info.Name}\")"); continue; } - wire.Connect(thisJunctionBox.Connections[i], addNode: false); - wire.Connect(previousJunctionBox.Connections[i], addNode: false); + wire.TryConnect(thisJunctionBox.Connections[i], addNode: false); + wire.TryConnect(previousJunctionBox.Connections[i], addNode: false); wire.SetNodes(new List()); } } @@ -1390,6 +1438,31 @@ namespace Barotrauma } } + private static void EnableFactionSpecificEntities(Submarine sub, Location location) + { + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != sub) { continue; } + + var layerAsIdentifier = me.Layer.ToIdentifier(); + if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) + { + me.HiddenInGame = + location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier]; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } + } + } + private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) { foreach (PlacedModule module in placedModules) @@ -1592,7 +1665,12 @@ namespace Barotrauma List killedCharacters = new List(); List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>(); - var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient); + + List factions = new List(); + if (location?.Faction != null) { factions.Add(location.Faction.Prefab); } + if (location?.SecondaryFaction != null) { factions.Add(location.SecondaryFaction.Prefab); } + + var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, Rand.RandSync.ServerAndClient); foreach (HumanPrefab humanPrefab in humanPrefabs) { if (humanPrefab is null) { continue; } @@ -1611,7 +1689,7 @@ namespace Barotrauma for (int tries = 0; tries < 100; tries++) { var characterInfo = killedCharacter.CreateCharacterInfo(Rand.RandSync.ServerAndClient); - if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) + if (location != null && !location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { selectedCharacters.Add((killedCharacter, characterInfo)); break; @@ -1633,22 +1711,21 @@ namespace Barotrauma npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; npc.HumanPrefab = humanPrefab; - if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) + outpost.Info.AddOutpostNPCIdentifierOrTag(npc, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) { - outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); + outpost.Info.AddOutpostNPCIdentifierOrTag(npc, tag); } - outpost.Info.OutpostNPCs[humanPrefab.Identifier].Add(npc); if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.KillableNPCs) { npc.CharacterHealth.Unkillable = true; } - humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); + humanPrefab.GiveItems(npc, outpost, gotoTarget as WayPoint, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; item.SpawnedInCurrentOutpost = true; } - npc.GiveIdCardTags(gotoTarget as WayPoint); humanPrefab.InitializeCharacter(npc, gotoTarget); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 794a9671b..9a9405020 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -93,13 +93,11 @@ namespace Barotrauma { moduleFlags.Add("hallwayhorizontal".ToIdentifier()); if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } - return; } if (newFlags.Contains("hallwayvertical".ToIdentifier())) { moduleFlags.Add("hallwayvertical".ToIdentifier()); if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } - return; } if (!newFlags.Any()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 532ce3957..696eda83f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -34,6 +34,13 @@ namespace Barotrauma /// public const int DefaultAmount = 5; + private readonly Dictionary minReputation = new Dictionary(); + + /// + /// Minimum reputation needed to buy the item (Key = faction ID, Value = min rep) + /// + public IReadOnlyDictionary MinReputation => minReputation; + /// /// Support for the old style of determining item prices /// when there were individual Price elements for each location type @@ -70,6 +77,19 @@ namespace Barotrauma RequiresUnlock = requiresUnlock; } + private void LoadReputationRestrictions(XElement priceInfoElement) + { + foreach (XElement childElement in priceInfoElement.GetChildElements("reputation")) + { + Identifier factionId = childElement.GetAttributeIdentifier("faction", Identifier.Empty); + float rep = childElement.GetAttributeFloat("min", 0.0f); + if (!factionId.IsEmpty && rep > 0) + { + minReputation.Add(factionId, rep); + } + } + } + public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { var priceInfos = new List(); @@ -106,6 +126,7 @@ namespace Barotrauma displayNonEmpty: displayNonEmpty, requiresUnlock: requiresUnlock, storeIdentifier: storeIdentifier); + priceInfo.LoadReputationRestrictions(childElement); priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); @@ -117,7 +138,8 @@ namespace Barotrauma minLevelDifficulty: minLevelDifficulty, buyingPriceMultiplier: buyingPriceMultiplier, displayNonEmpty: displayNonEmpty, - requiresUnlock: requiresUnlock); + requiresUnlock: requiresUnlock); + defaultPrice.LoadReputationRestrictions(element); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 8cd78e3f1..d27dd4471 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -81,6 +81,8 @@ namespace Barotrauma get { return base.Prefab.Sprite; } } + public bool IsExteriorWall { get; private set; } = true; + public bool IsPlatform { get { return Prefab.Platform; } @@ -402,6 +404,8 @@ namespace Barotrauma } } + CheckIsExteriorWall(); + #if CLIENT convexHulls?.ForEach(x => x.Move(amount)); @@ -697,15 +701,41 @@ namespace Barotrauma } } - private static Vector2[] CalculateExtremes(Rectangle sectionRect) + public void CheckIsExteriorWall() { - Vector2[] corners = new Vector2[4]; - corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height); - corners[1] = new Vector2(sectionRect.X, sectionRect.Y); - corners[2] = new Vector2(sectionRect.Right, sectionRect.Y); - corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height); + if (!HasBody) + { + IsExteriorWall = false; + return; + } - return corners; + Vector2 point1 = WorldPosition + BodyOffset * Scale; + //point1 = MathUtils.RotatePointAroundTarget(WorldPosition, point1, BodyRotation); + Vector2 point2 = point1; + + Vector2 normal = new Vector2( + (float)-Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), + (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)); + + float thickness = IsHorizontal ? + (BodyHeight > 0 ? BodyHeight : rect.Height) : + (BodyWidth > 0 ? BodyWidth : rect.Width); + + point1 += normal * (thickness / 2 + 16); + point2 -= normal * (thickness / 2 + 16); + + IsExteriorWall = + Hull.FindHullUnoptimized(point1, null, useWorldCoordinates: true) == null || + Hull.FindHullUnoptimized(point2, null, useWorldCoordinates: true) == null; +#if CLIENT + if (convexHulls != null) + { + foreach (ConvexHull ch in convexHulls) + { + ch.IsExteriorWall = IsExteriorWall; + } + } +#endif } /// @@ -715,7 +745,7 @@ namespace Barotrauma { foreach (MapEntity mapEntity in mapEntityList) { - if (!(mapEntity is Structure structure)) { continue; } + if (mapEntity is not Structure structure) { continue; } if (!structure.Prefab.AllowAttachItems) { continue; } if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; } Rectangle worldRect = mapEntity.WorldRect; @@ -939,7 +969,7 @@ namespace Barotrauma Rand.Range(worldRect.X, worldRect.Right + 1), Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); + var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; } } @@ -1085,9 +1115,9 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true) { - if (Submarine != null && Submarine.GodMode || Indestructible) { return; } + if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; } if (!Prefab.Body) { return; } if (!MathUtils.IsValid(damage)) { return; } @@ -1635,6 +1665,7 @@ namespace Barotrauma { SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false); } + CheckIsExteriorWall(); } public virtual void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 18e48a4c2..941ddab07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.IO; using System.Collections.Immutable; +using System.ComponentModel; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -44,30 +45,26 @@ namespace Barotrauma public override ImmutableHashSet Aliases { get; } - //does the structure have a physics body - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Does the structure have a physics body?")] public bool Body { get; private set; } - //rotation of the physics body in degrees - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the physics body in degrees.")] public float BodyRotation { get; private set; } - //in display units - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Width of the physics body in pixels.")] public float BodyWidth { get; private set; } - //in display units - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Height of the physics body in pixels.")] public float BodyHeight { get; private set; } //in display units - [Serialize("0.0,0.0", IsPropertySaveable.No)] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "Offset of the physics body from the center of the structure in pixels.")] public Vector2 BodyOffset { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Is the structure a platform (i.e. a \"floor\" the players can pass through)? Only relevant if the structure has a physics body.")] public bool Platform { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Can items like signal components be attached on this structure? Should be enabled on structures like decorative background walls.")] public bool AllowAttachItems { get; private set; } [Serialize(0.0f, IsPropertySaveable.No)] @@ -81,27 +78,30 @@ namespace Barotrauma private set { health = Math.Max(value, MinHealth); } } - [Serialize(true, IsPropertySaveable.No)] + [Serialize(true, IsPropertySaveable.No, description: "Should the structure be indestructible when used in an outpost?")] public bool IndestructibleInOutposts { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Should the structure cast shadows and obstruct visibility when LOS is enabled?")] public bool CastShadow { get; private set; } - [Serialize(Direction.None, IsPropertySaveable.No)] + [Serialize(Direction.None, IsPropertySaveable.No, description: "Makes the structure function as a staircase.")] public Direction StairDirection { get; private set; } - [Serialize(45.0f, IsPropertySaveable.No)] + [Serialize(45.0f, IsPropertySaveable.No, description: "Angle of the stairs in degrees. Only relevant if StairDirection is something else than None.")] public float StairAngle { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, monsters will not be able to target this structure.")] public bool NoAITarget { get; private set; } - [Serialize("0,0", IsPropertySaveable.Yes)] + [Serialize("0,0", IsPropertySaveable.Yes, description: "Size of the structure in pixels. If not set, the size is determined, based on the attributes width and height, and if those aren't defined either, based on the size of the structure's sprite.")] public Vector2 Size { get; private set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the sound that plays when something damages the wall.")] public string DamageSound { get; private set; } + [Serialize("shrapnel", IsPropertySaveable.Yes, description: "Identifier of the particles emitted when something damages the wall.")] + public string DamageParticle { get; private set; } + protected Vector2 textureScale = Vector2.One; [Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)] public Vector2 TextureScale @@ -175,7 +175,7 @@ namespace Barotrauma #endif foreach (var subElement in element.Elements()) { - switch (subElement.Name.ToString()) + switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": Sprite = new Sprite(subElement, lazyLoad: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index d5f4e7387..178edc186 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -213,9 +213,21 @@ namespace Barotrauma get { if (Level.Loaded == null) { return false; } - if (Level.Loaded.EndOutpost != null && DockedTo.Contains(Level.Loaded.EndOutpost)) + if (Level.Loaded.EndOutpost != null) { - return true; + if (DockedTo.Contains(Level.Loaded.EndOutpost)) + { + return true; + } + else if (Level.Loaded.EndOutpost.exitPoints.Any()) + { + return IsAtOutpostExit(Level.Loaded.EndOutpost); + } + } + else if (Level.Loaded.Type == LevelData.LevelType.Outpost && Level.Loaded.StartOutpost != null) + { + //in outpost levels, the outpost is always the start outpost: check it if has an exit + return IsAtOutpostExit(Level.Loaded.StartOutpost); } return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance); } @@ -226,14 +238,44 @@ namespace Barotrauma get { if (Level.Loaded == null) { return false; } - if (Level.Loaded.StartOutpost != null && DockedTo.Contains(Level.Loaded.StartOutpost)) + if (Level.Loaded.StartOutpost != null) { - return true; + if (DockedTo.Contains(Level.Loaded.StartOutpost)) + { + return true; + } + else if (Level.Loaded.StartOutpost.exitPoints.Any()) + { + return IsAtOutpostExit(Level.Loaded.StartOutpost); + } } return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartExitPosition) < Level.ExitDistance * Level.ExitDistance); } } + public bool AtEitherExit => AtStartExit || AtEndExit; + + private bool IsAtOutpostExit(Submarine outpost) + { + if (outpost.exitPoints.Any()) + { + Rectangle worldBorders = Borders; + worldBorders.Location += WorldPosition.ToPoint(); + foreach (var exitPoint in outpost.exitPoints) + { + if (exitPoint.ExitPointSize != Point.Zero) + { + if (RectsOverlap(worldBorders, exitPoint.ExitPointWorldRect)) { return true; } + } + else + { + if (RectContains(worldBorders, exitPoint.WorldPosition)) { return true; } + } + } + } + return false; + } + public new Vector2 DrawPosition { @@ -284,6 +326,9 @@ namespace Barotrauma } } + private readonly List exitPoints = new List(); + public IReadOnlyList ExitPoints { get { return exitPoints; } } + public override string ToString() { return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")"; @@ -350,12 +395,23 @@ namespace Barotrauma } public WreckAI WreckAI { get; private set; } + public SubmarineTurretAI TurretAI { get; private set; } + public bool CreateWreckAI() { WreckAI = WreckAI.Create(this); return WreckAI != null; } + /// + /// Creates an AI that operates all the turrets on a sub, same as Thalamus but only operates the turrets. + /// + public bool CreateTurretAI() + { + TurretAI = new SubmarineTurretAI(this); + return TurretAI != null; + } + public void DisableWreckAI() { if (WreckAI == null) @@ -991,6 +1047,7 @@ namespace Barotrauma { WreckAI?.Update(deltaTime); } + TurretAI?.Update(deltaTime); if (subBody?.Body == null) { return; } @@ -1038,7 +1095,7 @@ namespace Barotrauma public void ApplyForce(Vector2 force) { - if (subBody != null) subBody.ApplyForce(force); + if (subBody != null) { subBody.ApplyForce(force); } } public void EnableMaintainPosition() @@ -1271,22 +1328,38 @@ namespace Barotrauma } /// - /// Finds the sub whose borders contain the position + /// Finds the sub whose borders contain the position. Note that this method uses the "actual" position of the sub outside the level: + /// only use this if the position is in a submarine's local coordinate space! /// - public static Submarine FindContaining(Vector2 position) + public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f) { foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; - subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); - - subBorders.Inflate(500.0f, 500.0f); - - if (subBorders.Contains(position)) return sub; + subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height); + subBorders.Inflate(inflate, inflate); + if (subBorders.Contains(position)) { return sub; } } return null; } + + /// + /// Finds the sub whose world borders contain the position. + /// + public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f) + { + foreach (Submarine sub in Loaded) + { + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint(); + worldBorders.Inflate(inflate, inflate); + if (RectContains(worldBorders, worldPosition)) { return sub; } + } + return null; + } + + public static Rectangle GetBorders(XElement submarineElement) { Vector4 bounds = Vector4.Zero; @@ -1325,14 +1398,19 @@ namespace Barotrauma //place the sub above the top of the level HiddenSubPosition = HiddenSubStartPosition; - if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) + if (GameMain.GameSession?.LevelData != null) { HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; } - foreach (Submarine sub in loaded) + for (int i = 0; i < loaded.Count; i++) { - HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); + Submarine sub = loaded[i]; + HiddenSubPosition = + new Vector2( + //1st sub on the left side, 2nd on the right, etc + HiddenSubPosition.X * (i % 2 == 0 ? 1 : -1), + HiddenSubPosition.Y + sub.Borders.Height + 5000.0f); } IdOffset = IdRemap.DetermineNewOffset(); @@ -1460,10 +1538,15 @@ namespace Barotrauma MapEntity.MapLoaded(newEntities, true); foreach (MapEntity me in MapEntity.mapEntityList) { - if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + if (me.Submarine != this) { continue; } + if (me is LinkedSubmarine linkedSub) { linkedSub.LinkDummyToMainSubmarine(); } + else if (me is WayPoint wayPoint && wayPoint.SpawnType.HasFlag(SpawnType.ExitPoint)) + { + exitPoints.Add(wayPoint); + } } foreach (Hull hull in matchingHulls) @@ -1478,6 +1561,7 @@ namespace Barotrauma #if CLIENT GameMain.LightManager.OnMapLoaded(); + Lights.ConvexHull.RecalculateAll(this); #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula @@ -1683,57 +1767,66 @@ namespace Barotrauma public static void Unload() { + if (Unloading) + { + DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading."); + return; + } + Unloading = true; + try + { #if CLIENT - RoundSound.RemoveAllRoundSounds(); - GameMain.LightManager?.ClearLights(); + RoundSound.RemoveAllRoundSounds(); + GameMain.LightManager?.ClearLights(); #endif - var _loaded = new List(loaded); - foreach (Submarine sub in _loaded) - { - sub.Remove(); - } - - loaded.Clear(); - - visibleEntities = null; - - if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } - - RemoveAll(); - - if (Item.ItemList.Count > 0) - { - List items = new List(Item.ItemList); - foreach (Item item in items) + var _loaded = new List(loaded); + foreach (Submarine sub in _loaded) { - DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); - try - { - item.Remove(); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); - } + sub.Remove(); } - Item.ItemList.Clear(); + + loaded.Clear(); + + visibleEntities = null; + + if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } + + RemoveAll(); + + if (Item.ItemList.Count > 0) + { + List items = new List(Item.ItemList); + foreach (Item item in items) + { + DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); + try + { + item.Remove(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); + } + } + Item.ItemList.Clear(); + } + + Ragdoll.RemoveAll(); + PhysicsBody.RemoveAll(); + GameMain.World = null; + + Powered.Grids.Clear(); + + GC.Collect(); + + } + finally + { + Unloading = false; } - - Ragdoll.RemoveAll(); - - PhysicsBody.RemoveAll(); - - GameMain.World?.Clear(); - GameMain.World = null; - - Powered.Grids.Clear(); - - GC.Collect(); - - Unloading = false; } public override void Remove() @@ -1800,18 +1893,18 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed) { continue; } + if (connectedWp.IsObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition); var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false); if (body != null) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; break; } } @@ -1830,11 +1923,11 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; } + if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); @@ -1842,8 +1935,8 @@ namespace Barotrauma { if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { nodes = new HashSet(); @@ -1865,7 +1958,7 @@ namespace Barotrauma { if (obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { - nodes.ForEach(n => n.Waypoint.isObstructed = false); + nodes.ForEach(n => n.Waypoint.IsObstructed = false); nodes.Clear(); obstructedNodes.Remove(otherSub); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 7867f1166..4bf6ca6e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -18,6 +18,13 @@ namespace Barotrauma { public const float NeutralBallastPercentage = 0.07f; + public const Category CollidesWith = + Physics.CollisionItem | + Physics.CollisionLevel | + Physics.CollisionCharacter | + Physics.CollisionProjectile | + Physics.CollisionWall; + const float HorizontalDrag = 0.01f; const float VerticalDrag = 0.05f; const float MaxDrag = 0.1f; @@ -32,6 +39,8 @@ namespace Barotrauma private const float MaxCollisionImpact = 5.0f; private const float Friction = 0.2f, Restitution = 0.0f; + private readonly List levelContacts = new List(); + public List HullVertices { get; @@ -39,6 +48,7 @@ namespace Barotrauma } private float depthDamageTimer = 10.0f; + private float damageSoundTimer = 10.0f; private readonly Submarine submarine; @@ -48,6 +58,9 @@ namespace Barotrauma private readonly Queue impactQueue = new Queue(); + private float forceUpwardsTimer; + private const float ForceUpwardsDelay = 30.0f; + struct Impact { public Fixture Target; @@ -146,9 +159,13 @@ namespace Barotrauma farseerBody.CollidesWith = collidesWith; farseerBody.Enabled = false; farseerBody.UserData = this; + if (sub.Info.IsOutpost) + { + farseerBody.BodyType = BodyType.Static; + } foreach (var mapEntity in MapEntity.mapEntityList) { - if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; } + if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; } bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; @@ -185,13 +202,20 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != submarine) { continue; } - if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; } + + Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); + if (item.GetComponent() is Door door) + { + door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith); + door.OutsideSubmarineFixture.UserData = item; + } + + if (item.StaticBodyConfig == null) { continue; } float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale; float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale; float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale; - Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); float simRadius = ConvertUnits.ToSimUnits(radius); float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); @@ -374,6 +398,33 @@ namespace Barotrauma //------------------------- + //if heading up and there's another sub on top of us, gradually force it upwards + //(i.e. apply "artificial buoyancy" to it) to prevent us from getting pinned under it + //only applies to enemy subs with no enemies inside them (like destroyed pirate subs) + if (totalForce.Y > 0) + { + ContactEdge contactEdge = Body?.FarseerBody?.ContactList; + while (contactEdge?.Next != null) + { + if (contactEdge.Contact.Enabled && + contactEdge.Other.UserData is Submarine otherSubmarine && + otherSubmarine.TeamID != Submarine.TeamID && + contactEdge.Contact.IsTouching) + { + contactEdge.Contact.GetWorldManifold(out Vector2 _, out FixedArray2 points); + if (points[0].Y > Body.SimPosition.Y && + !Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID)) + { + otherSubmarine.SubBody.forceUpwardsTimer += deltaTime; + break; + } + } + contactEdge = contactEdge.Next; + } + } + + //------------------------- + if (Body.LinearVelocity.LengthSquared() > 0.0001f) { //TODO: sync current drag with clients? @@ -397,7 +448,34 @@ namespace Barotrauma ApplyForce(totalForce); + if (Velocity.LengthSquared() < 0.01f) + { + levelContacts.Clear(); + levelContacts.AddRange(GetLevelContacts(Body)); + for (int i = 0; i < levelContacts.Count; i++) + { + for (int j = i + 1; j < levelContacts.Count; j++) + { + levelContacts[i].GetWorldManifold(out Vector2 normal1, out _); + levelContacts[j].GetWorldManifold(out Vector2 normal2, out _); + + //normals pointing in different directions = sub lodged between two walls + if (Vector2.Dot(normal1, normal2) < 0) + { + //apply an extra force to hopefully dislodge the sub + ApplyForce(totalForce * 100.0f); + i = levelContacts.Count; + break; + } + } + } + } + + totalForcePerFrame = Vector2.Zero; + UpdateDepthDamage(deltaTime); + + forceUpwardsTimer = MathHelper.Clamp(forceUpwardsTimer - deltaTime * 0.1f, 0.0f, ForceUpwardsDelay); } partial void ClientUpdatePosition(float deltaTime); @@ -464,16 +542,28 @@ namespace Barotrauma float buoyancy = NeutralBallastPercentage - waterPercentage; if (buoyancy > 0.0f) + { buoyancy *= 2.0f; + } else + { buoyancy = Math.Max(buoyancy, -0.5f); + } + + if (forceUpwardsTimer > 0.0f) + { + buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); + } return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f); } + + private Vector2 totalForcePerFrame; public void ApplyForce(Vector2 force) { Body.ApplyForce(force); + totalForcePerFrame += force; } public void SetPosition(Vector2 position) @@ -489,36 +579,58 @@ namespace Barotrauma if (Level.Loaded == null) { return; } //camera shake and sounds start playing 500 meters before crush depth - float depthEffectThreshold = 500.0f; - if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold) + const float CosmeticEffectThreshold = -500.0f; + //breaches won't get any more severe 500 meters below crush depth + const float MaxEffectThreshold = 500.0f; + const float MinWallDamageProbability = 0.1f; + const float MaxWallDamageProbability = 1.0f; + const float MinWallDamage = 50f; + const float MaxWallDamage = 500.0f; + const float MinCameraShake = 5f; + const float MaxCameraShake = 50.0f; + + if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold) { return; } - depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) { return; } - -#if CLIENT - SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); -#endif - - foreach (Structure wall in Structure.WallList) + damageSoundTimer -= deltaTime; + if (damageSoundTimer <= 0.0f) { - if (wall.Submarine != submarine) { continue; } - - float wallCrushDepth = wall.CrushDepth; - float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; - if (pastCrushDepth > 0) - { - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); - } - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) - { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f)); - } +#if CLIENT + SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); +#endif + damageSoundTimer = Rand.Range(5.0f, 10.0f); } - depthDamageTimer = 10.0f; + depthDamageTimer -= deltaTime; + if (depthDamageTimer <= 0.0f) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != submarine) { continue; } + + float wallCrushDepth = wall.CrushDepth; + float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; + float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f); + + if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; } + + float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio); + if (pastCrushDepth > 0) + { + Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f); +#if CLIENT + SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f); +#endif + } + if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + { + GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio)); + } + } + depthDamageTimer = Rand.Range(5.0f, 10.0f); + } } public void FlipX() @@ -623,7 +735,7 @@ namespace Barotrauma attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier; } - if (impactMass * attackMultiplier > MinImpactLimbMass) + if (impactMass * attackMultiplier > MinImpactLimbMass && Body.BodyType != BodyType.Static) { Vector2 normal = Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? @@ -641,20 +753,10 @@ namespace Barotrauma } //find all contacts between the limb and level walls - List levelContacts = new List(); - ContactEdge contactEdge = limb.body.FarseerBody.ContactList; - while (contactEdge?.Contact != null) - { - if (contactEdge.Contact.Enabled && - contactEdge.Contact.IsTouching && - contactEdge.Other?.UserData is VoronoiCell) - { - levelContacts.Add(contactEdge.Contact); - } - contactEdge = contactEdge.Next; - } + IEnumerable levelContacts = GetLevelContacts(limb.body); + int levelContactCount = levelContacts.Count(); - if (levelContacts.Count == 0) { return; } + if (levelContactCount == 0) { return; } //if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it //not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete), @@ -677,9 +779,9 @@ namespace Barotrauma avgContactNormal += contactNormal; //apply impacts at the positions where this sub is touching the limb - ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContacts.Count, contactNormal, collision.ImpactPos, applyDamage: false); + ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContactCount, contactNormal, collision.ImpactPos, applyDamage: false); } - avgContactNormal /= levelContacts.Count; + avgContactNormal /= levelContactCount; float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal); if (contactDot > 0.001f) @@ -718,6 +820,21 @@ namespace Barotrauma } } + private IEnumerable GetLevelContacts(PhysicsBody body) + { + ContactEdge contactEdge = body.FarseerBody.ContactList; + while (contactEdge?.Contact != null) + { + if (contactEdge.Contact.Enabled && + contactEdge.Contact.IsTouching && + contactEdge.Other?.UserData is VoronoiCell) + { + yield return contactEdge.Contact; + } + contactEdge = contactEdge.Next; + } + } + private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10) @@ -786,21 +903,9 @@ namespace Barotrauma } //find all contacts between this sub and level walls - List levelContacts = new List(); - ContactEdge contactEdge = Body?.FarseerBody?.ContactList; - while (contactEdge?.Next != null) - { - if (contactEdge.Contact.Enabled && - contactEdge.Other.UserData is VoronoiCell && - contactEdge.Contact.IsTouching) - { - levelContacts.Add(contactEdge.Contact); - } - - contactEdge = contactEdge.Next; - } - - if (levelContacts.Count == 0) { return; } + IEnumerable levelContacts = GetLevelContacts(Body); + int levelContactCount = levelContacts.Count(); + if (levelContactCount == 0) { return; } //if this sub is in contact with the level, apply artifical impacts //to both subs to prevent the other sub from bouncing on top of this one @@ -811,8 +916,7 @@ namespace Barotrauma levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2 temp); //if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal - VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ? - ((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData); + VoronoiCell cell = levelContact.FixtureB.UserData as VoronoiCell ?? levelContact.FixtureA.UserData as VoronoiCell; var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center; if (Vector2.Dot(contactNormal, cellDiff) < 0) @@ -823,9 +927,9 @@ namespace Barotrauma avgContactNormal += contactNormal; //apply impacts at the positions where this sub is touching the level - ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContacts.Count, contactNormal, impact.ImpactPos); + ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContactCount, contactNormal, impact.ImpactPos); } - avgContactNormal /= levelContacts.Count; + avgContactNormal /= levelContactCount; //apply an impact to the other sub float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a5c3ffac7..a83578cfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; #if DEBUG using System.IO; @@ -478,7 +479,6 @@ namespace Barotrauma hashTask = new Task(() => { hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace); - Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow); }); hashTask.Start(); } @@ -559,6 +559,14 @@ namespace Barotrauma } return structureCrushDepthsDefined; } + public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag) + { + if (!OutpostNPCs.ContainsKey(idOrTag)) + { + OutpostNPCs.Add(idOrTag, new List()); + } + OutpostNPCs[idOrTag].Add(npc); + } //saving/loading ---------------------------------------------------- public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null) @@ -590,7 +598,6 @@ namespace Barotrauma } SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.Cache.Remove(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) @@ -752,6 +759,36 @@ namespace Barotrauma return doc; } + public int GetPrice(Location location = null, ImmutableHashSet characterList = null) + { + if (location is null) + { + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation is { } currentLocation) + { + location = currentLocation; + } + else + { + + return Price; + } + } + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + float price = Price; + if (characterList.Any()) + { + if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); + } + + return (int)price; + } + public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1; public const int HighestTier = 3; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 1a99f675a..2bb863cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -11,7 +11,7 @@ using Barotrauma.Extensions; namespace Barotrauma { [Flags] - public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 }; + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32 }; partial class WayPoint : MapEntity { @@ -29,7 +29,32 @@ namespace Barotrauma private HashSet tags; - public bool isObstructed; + public bool IsObstructed; + + public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y; + + // Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open. + // The only way to add the open gaps should be by calling OnGapStateSchanged. + public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater); + + private HashSet openGaps; + /// + /// Only called by a Gap when the state changes. + /// So in practice used like an event callback, although technically just a method + /// (It would be cleaner to use an actual event in Gap.cs, but event registering and unregistering might cause an extra hassle) + /// + public void OnGapStateChanged(bool open, Gap gap) + { + openGaps ??= new HashSet(); + if (open) + { + openGaps.Add(gap); + } + else + { + openGaps.Remove(gap); + } + } private ushort gapId; public Gap ConnectedGap @@ -54,6 +79,12 @@ namespace Barotrauma set { spawnType = value; } } + public Point ExitPointSize { get; private set; } + + public Rectangle ExitPointWorldRect => new Rectangle( + (int)WorldPosition.X - ExitPointSize.X / 2, (int)WorldPosition.Y + ExitPointSize.Y / 2, + ExitPointSize.X, ExitPointSize.Y); + public Action OnLinksChanged { get; set; } public override string Name @@ -140,7 +171,9 @@ namespace Barotrauma { "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, { "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) }, { "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) }, - { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) } + { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) }, + { "Submarine", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) }, + { "ExitPoint", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) } }; } #endif @@ -981,6 +1014,12 @@ namespace Barotrauma public override void OnMapLoaded() { + if (Submarine == null) + { + // Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc. + // Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost. + return; + } InitializeLinks(); FindHull(); FindStairs(); @@ -1018,7 +1057,6 @@ namespace Barotrauma int.Parse(element.GetAttribute("y").Value), (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y); - Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { @@ -1036,6 +1074,8 @@ namespace Barotrauma w.IdCardTags = idCardTagString.Split(','); } + w.ExitPointSize = element.GetAttributePoint("exitpointsize", Point.Zero); + w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); Identifier jobIdentifier = element.GetAttributeIdentifier("job", Identifier.Empty); @@ -1076,6 +1116,10 @@ namespace Barotrauma new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)), new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)), new XAttribute("spawn", spawnType)); + if (SpawnType == SpawnType.ExitPoint) + { + element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize))); + } if (!string.IsNullOrWhiteSpace(IdCardDesc)) element.Add(new XAttribute("idcarddesc", IdCardDesc)); if (idCardTags.Length > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index d453464f8..e88558001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -87,6 +87,7 @@ namespace Barotrauma.Networking if (character != null) { HasSpawned = true; + UsingFreeCam = false; #if CLIENT GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally); @@ -100,6 +101,11 @@ namespace Barotrauma.Networking } } + /// + /// Is the client using the 'freecam' console command? + /// + public bool UsingFreeCam; + public UInt16 CharacterID; private Vector2 spectatePos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 8bde51456..b17701a49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -28,28 +28,30 @@ namespace Barotrauma.Networking ManageMap = 0x8000, ManageHires = 0x10000, ManageBotTalents = 0x20000, - All = 0x3FFFF + SpamImmunity = 0x40000, + All = 0x7FFFF } class PermissionPreset { public static readonly List List = new List(); - - public readonly LocalizedString Name; + + public readonly Identifier Identifier; + public readonly LocalizedString DisplayName; public readonly LocalizedString Description; public readonly ClientPermissions Permissions; public readonly HashSet PermittedCommands; public PermissionPreset(XElement element) { - string name = element.GetAttributeString("name", ""); - Name = TextManager.Get("permissionpresetname." + name).Fallback(name); - Description = TextManager.Get("permissionpresetdescription." + name) .Fallback(element.GetAttributeString("description", "")); + Identifier = element.GetAttributeIdentifier("name", Identifier.Empty); + DisplayName = TextManager.Get("permissionpresetname." + Identifier).Fallback(Identifier.ToString()); + Description = TextManager.Get("permissionpresetdescription." + Identifier) .Fallback(element.GetAttributeString("description", "")); string permissionsStr = element.GetAttributeString("permissions", ""); if (!Enum.TryParse(permissionsStr, out Permissions)) { - DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + permissionsStr + " is not a valid permission!"); + DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + permissionsStr + " is not a valid permission!"); } PermittedCommands = new HashSet(); @@ -64,7 +66,7 @@ namespace Barotrauma.Networking if (command == null) { #if SERVER - DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + commandName + "\" is not a valid console command."); + DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + commandName + "\" is not a valid console command."); #endif continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index 29855637a..cba0112e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -348,12 +348,7 @@ namespace Barotrauma } private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : struct => - ReadOption(inc, attribute, bitField) switch - { - Some { Value: var value } => value, - None _ => null, - _ => throw new ArgumentOutOfRangeException() - }; + ReadOption(inc, attribute, bitField).TryUnwrap(out var value) ? value : null; private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : struct => WriteOption(value.HasValue ? Option.Some(value.Value) : Option.None(), attribute, msg, bitField); @@ -378,7 +373,7 @@ namespace Barotrauma { ToolBox.ThrowIfNull(option); - if (option.TryUnwrap(out T value)) + if (option.TryUnwrap(out T? value)) { bitField.WriteBoolean(true); if (TryFindBehavior(out ReadWriteBehavior behavior)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index d4b897c9c..0f33d9599 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking /// public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + order?.GetChatMessage(targetCharacter?.Name, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) { @@ -110,7 +110,7 @@ namespace Barotrauma.Networking WriteOrder(msg, Order, TargetCharacter, IsNewOrder); } - public struct OrderMessageInfo + public readonly struct OrderMessageInfo { public Identifier OrderIdentifier { get; } public OrderPrefab OrderPrefab => OrderPrefab.Prefabs[OrderIdentifier]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index fcc0bed07..6f5bf9235 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -39,6 +39,7 @@ namespace Barotrauma.Networking ServerMessage, ConsoleUsage, Money, + DoSProtection, Karma, Talent, Error, @@ -55,6 +56,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, { MessageType.Money, Color.Green }, + { MessageType.DoSProtection, Color.OrangeRed }, { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Talent, new Color(125, 125, 255) }, { MessageType.Error, Color.Red } @@ -71,6 +73,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, { MessageType.Money, "Money" }, + { MessageType.DoSProtection, "DoSProtection" }, { MessageType.Karma, "Karma" }, { MessageType.Talent, "Talent" }, { MessageType.Error, "Error" } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 01cbd8d0c..d4ed2e781 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -42,6 +42,11 @@ namespace Barotrauma.Networking partial class ServerSettings : ISerializableEntity { + public const int PacketLimitMin = 1200, + PacketLimitWarning = 2400, + PacketLimitDefault = 2400, + PacketLimitMax = 10000; + public const string SettingsFile = "serversettings.xml"; [Flags] @@ -111,27 +116,24 @@ namespace Barotrauma.Networking switch (typeString) { case "float": - if (!(a is float?)) return false; - if (!(b is float?)) return false; - return MathUtils.NearlyEqual((float)a, (float)b); + if (a is not float fa) { return false; } + if (b is not float fb) { return false; } + return MathUtils.NearlyEqual(fa, fb); case "int": - if (!(a is int?)) return false; - if (!(b is int?)) return false; - return (int)a == (int)b; + if (a is not int ia) { return false; } + if (b is not int ib) { return false; } + return ia == ib; case "bool": - if (!(a is bool?)) return false; - if (!(b is bool?)) return false; - return (bool)a == (bool)b; + if (a is not bool ba) { return false; } + if (b is not bool bb) { return false; } + return ba == bb; case "Enum": - if (!(a is Enum)) return false; - if (!(b is Enum)) return false; - return ((Enum)a).Equals((Enum)b); + if (a is not Enum ea) { return false; } + if (b is not Enum eb) { return false; } + return ea.Equals(eb); default: - if (a == null || b == null) - { - return (a == null) == (b == null); - } - return a.ToString().Equals(b.ToString(), StringComparison.OrdinalIgnoreCase); + return ReferenceEquals(a,b) + || string.Equals(a?.ToString(), b?.ToString(), StringComparison.OrdinalIgnoreCase); } } @@ -204,7 +206,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, object overrideValue = null) { - if (overrideValue == null) { overrideValue = Value; } + overrideValue ??= Value; switch (typeString) { case "float": @@ -293,10 +295,7 @@ namespace Barotrauma.Networking var saveProperties = SerializableProperty.GetProperties(this); foreach (var property in saveProperties) { - object value = property.GetValue(this); - if (value == null) { continue; } - - string typeName = SerializableProperty.GetSupportedTypeName(value.GetType()); + string typeName = SerializableProperty.GetSupportedTypeName(property.PropertyType); if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); @@ -523,7 +522,7 @@ namespace Barotrauma.Networking } } - [Serialize(LosMode.Opaque, IsPropertySaveable.Yes)] + [Serialize(LosMode.Transparent, IsPropertySaveable.Yes)] public LosMode LosMode { get; @@ -694,6 +693,20 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool EnableDoSProtection + { + get; + private set; + } + + [Serialize(PacketLimitDefault, IsPropertySaveable.Yes)] + public int MaxPacketAmount + { + get; + private set; + } + [Serialize("", IsPropertySaveable.Yes)] public string SelectedSubmarine { @@ -754,6 +767,9 @@ namespace Barotrauma.Networking get; set; } + + [Serialize(defaultValue: "", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 14ae70d17..72e237b6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System; using System.Collections.Generic; namespace Barotrauma diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 8286eee68..c4873a2cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -68,7 +68,7 @@ namespace Barotrauma public void TransformInToOutside() { - var sub = Submarine.FindContaining(ConvertUnits.ToDisplayUnits(Position)); + var sub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(Position)); if (sub != null) { Position += ConvertUnits.ToSimUnits(sub.Position); @@ -119,7 +119,9 @@ namespace Barotrauma } private Shape bodyShape; - public float height, width, radius; + public float Height { get; private set; } + public float Width { get; private set; } + public float Radius { get; private set; } private readonly float density; @@ -385,7 +387,7 @@ namespace Barotrauma float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; density = Physics.NeutralDensity; - CreateBody(width, height, radius, density, BodyType.Dynamic, + CreateBody(width, height, radius, density, colliderParams.BodyType, Physics.CollisionCharacter, Physics.CollisionWall | Physics.CollisionLevel, findNewContacts); @@ -434,9 +436,17 @@ namespace Barotrauma float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); - _collisionCategories = collisionCategory; - _collidesWith = collidesWith; + if (element.GetAttributeBool("ignorecollision", false)) + { + _collisionCategories = Category.None; + _collidesWith = Category.None; + } + else + { + _collisionCategories = collisionCategory; + _collidesWith = collidesWith; + } + CreateBody(width, height, radius, density, bodyType, _collisionCategories, _collidesWith, findNewContacts); FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; @@ -472,9 +482,9 @@ namespace Barotrauma { DebugConsole.ThrowError("Invalid physics body dimensions (width: " + width + ", height: " + height + ", radius: " + radius + ")"); } - this.width = width; - this.height = height; - this.radius = radius; + Width = width; + Height = height; + Radius = radius; _collisionCategories = collisionCategory; _collidesWith = collidesWith; } @@ -492,16 +502,16 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - pos = new Vector2(0.0f, height / 2 + radius); + pos = new Vector2(0.0f, Height / 2 + Radius); break; case Shape.HorizontalCapsule: - pos = new Vector2(width / 2 + radius, 0.0f); + pos = new Vector2(Width / 2 + Radius, 0.0f); break; case Shape.Circle: - pos = new Vector2(0.0f, radius); + pos = new Vector2(0.0f, Radius); break; case Shape.Rectangle: - pos = height > width ? new Vector2(0, height / 2) : new Vector2(width / 2, 0); + pos = Height > Width ? new Vector2(0, Height / 2) : new Vector2(Width / 2, 0); break; default: throw new NotImplementedException(); @@ -514,13 +524,13 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - return height / 2 + radius; + return Height / 2 + Radius; case Shape.HorizontalCapsule: - return width / 2 + radius; + return Width / 2 + Radius; case Shape.Circle: - return radius; + return Radius; case Shape.Rectangle: - return new Vector2(width * 0.5f, height * 0.5f).Length(); + return new Vector2(Width * 0.5f, Height * 0.5f).Length(); default: throw new NotImplementedException(); } @@ -531,13 +541,13 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - return new Vector2(radius * 2, height + radius * 2); + return new Vector2(Radius * 2, Height + Radius * 2); case Shape.HorizontalCapsule: - return new Vector2(width + radius * 2, radius * 2); + return new Vector2(Width + Radius * 2, Radius * 2); case Shape.Circle: - return new Vector2(radius * 2); + return new Vector2(Radius * 2); case Shape.Rectangle: - return new Vector2(width, height); + return new Vector2(Width, Height); default: throw new NotImplementedException(); } @@ -548,24 +558,24 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - radius = Math.Max(size.X / 2, 0); - height = Math.Max(size.Y - size.X, 0); - width = 0; + Radius = Math.Max(size.X / 2, 0); + Height = Math.Max(size.Y - size.X, 0); + Width = 0; break; case Shape.HorizontalCapsule: - radius = Math.Max(size.Y / 2, 0); - width = Math.Max(size.X - size.Y, 0); - height = 0; + Radius = Math.Max(size.Y / 2, 0); + Width = Math.Max(size.X - size.Y, 0); + Height = 0; break; case Shape.Circle: - radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); - width = 0; - height = 0; + Radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); + Width = 0; + Height = 0; break; case Shape.Rectangle: - width = Math.Max(size.X, 0); - height = Math.Max(size.Y, 0); - radius = 0; + Width = Math.Max(size.X, 0); + Height = Math.Max(size.Y, 0); + Radius = 0; break; default: throw new NotImplementedException(); @@ -830,7 +840,7 @@ namespace Barotrauma Vector2 velDir = LinearVelocity / speed; float vel = speed * 2.0f; - float drag = vel * vel * Math.Max(height + radius * 2, height); + float drag = vel * vel * Math.Max(Height + Radius * 2, Height); dragForce = Math.Min(drag, Mass * 500.0f) * -velDir; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs index e5bf41bca..aa877db24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,6 +12,8 @@ namespace Barotrauma { public Identifier VariantOf { get; } + public T? ParentPrefab { get; set; } + public void InheritFrom(T parent); } @@ -20,8 +24,10 @@ namespace Barotrauma #warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage); } - - public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement) + + public delegate void VariantXMLChecker(XElement originalElement, XElement? variantElement, XElement result); + + public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement, VariantXMLChecker? checker = null) { XElement newElement = new XElement(variantElement.Name); newElement.Add(baseElement.Attributes()); @@ -31,6 +37,9 @@ namespace Barotrauma void ReplaceElement(XElement element, XElement replacement) { + XElement originalElement = new XElement(element); + + List newElementsFromBase = new List(element.Elements()); List elementsToRemove = new List(); foreach (XAttribute attribute in replacement.Attributes()) { @@ -48,6 +57,7 @@ namespace Barotrauma if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase)) { matchingElementFound = true; + newElementsFromBase.Clear(); elementsToRemove.AddRange(element.Elements()); break; } @@ -65,6 +75,7 @@ namespace Barotrauma ReplaceElement(subElement, replacementSubElement); } matchingElementFound = true; + newElementsFromBase.Remove(subElement); break; } i++; @@ -75,11 +86,16 @@ namespace Barotrauma } } elementsToRemove.ForEach(e => e.Remove()); + checker?.Invoke(originalElement, replacement, element); + foreach (XElement newElement in newElementsFromBase) + { + checker?.Invoke(newElement, null, newElement); + } } void ReplaceAttribute(XElement element, XAttribute newAttribute) { - XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + XAttribute? existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); if (existingAttribute == null) { element.Add(newAttribute); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 4b3b7edc9..e11ea36db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -125,7 +125,7 @@ namespace Barotrauma public Node? AddNodeAndInheritors(Identifier id) { - if (!prefabCollection.TryGet(id, out T? prefab)) { return null; } + if (!prefabCollection.TryGet(id, out T? _, requireInheritanceValid: false)) { return null; } if (!IdToNode.TryGetValue(id, out var node)) { @@ -139,24 +139,25 @@ namespace Barotrauma //all inheritors so let's just return this immediately return node; } - - prefabCollection - .Cast>() - .Where(p => p.VariantOf == id) - .Cast() - .ForEach(p => - { - var inheritorNode = AddNodeAndInheritors(p.Identifier); - if (inheritorNode is null) { return; } - RootNodes.Remove(inheritorNode); - inheritorNode.Parent = node; - node.Inheritors.Add(inheritorNode); - }); + var enumerator = prefabCollection.GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is not IImplementsVariants implementsVariants || implementsVariants.VariantOf != id) + { + continue; + } + var inheritorNode = AddNodeAndInheritors(p.Identifier); + if (inheritorNode is null) { continue; } + RootNodes.Remove(inheritorNode); + inheritorNode.Parent = node; + node.Inheritors.Add(inheritorNode); + } return node; } - private void FindCycles(in Node node, HashSet uncheckedNodes) + private static void FindCycles(in Node node, HashSet uncheckedNodes) { HashSet checkedNodes = new HashSet(); List hierarchyPositions = new List(); @@ -183,24 +184,45 @@ namespace Barotrauma public void InvokeCallbacks() { HashSet uncheckedNodes = IdToNode.Values.ToHashSet(); - IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes)); + IdToNode.Values.ForEach(v => PrefabCollection.InheritanceTreeCollection.FindCycles(v, uncheckedNodes)); void invokeCallbacksForNode(Node node) { - if (!prefabCollection.TryGet(node.Identifier, out var p) || - !(p is IImplementsVariants prefab)) { return; } - if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); } + if (!prefabCollection.TryGet(node.Identifier, out var p, requireInheritanceValid: false) || + p is not IImplementsVariants prefab) { return; } + if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent, requireInheritanceValid: false)) + { + prefab.InheritFrom(parent); + prefab.ParentPrefab = parent; + } node.Inheritors.ForEach(invokeCallbacksForNode); } RootNodes.ForEach(invokeCallbacksForNode); } } + private static bool IsInheritanceValid(T? prefab) + { + if (prefab == null) { return false; } + return + prefab is not IImplementsVariants implementsVariants || + (implementsVariants.VariantOf.IsEmpty || (implementsVariants.ParentPrefab != null && IsInheritanceValid(implementsVariants.ParentPrefab))); + } + private void HandleInheritance(Identifier prefabIdentifier) => HandleInheritance(prefabIdentifier.ToEnumerable()); private void HandleInheritance(IEnumerable identifiers) { if (!implementsVariants) { return; } + foreach (var id in identifiers) + { + if (!TryGet(id, out T? prefab, requireInheritanceValid: false)) { continue; } + if (prefab is IImplementsVariants implementsVariants && !implementsVariants.VariantOf.IsEmpty) + { + //reset parent prefab, it'll get set in InvokeCallbacks if the inheritance is valid + implementsVariants.ParentPrefab = null; + } + } InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this); inheritanceTreeCollection.AddNodesAndInheritors(identifiers); inheritanceTreeCollection.InvokeCallbacks(); @@ -213,9 +235,11 @@ namespace Barotrauma { get { - foreach (var prefab in prefabs) + foreach (var kvp in prefabs) { - yield return prefab; + var prefab = kvp.Value.ActivePrefab; + if (!IsInheritanceValid(prefab)) { continue; } + yield return kvp; } } } @@ -231,7 +255,8 @@ namespace Barotrauma { Prefab.DisallowCallFromConstructor(); var prefab = prefabs[identifier].ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + if (prefab != null && !IsPrefabOverriddenByFile(prefab) && + IsInheritanceValid(prefab)) { return prefab; } @@ -258,12 +283,17 @@ namespace Barotrauma /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) + { + return TryGet(identifier, out result, requireInheritanceValid: true); + } + + private bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result, bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) + if (prefabs.TryGetValue(identifier, out PrefabSelector? selector) && selector.ActivePrefab != null) { result = selector!.ActivePrefab; - return true; + return !requireInheritanceValid || IsInheritanceValid(result); } else { @@ -304,7 +334,7 @@ namespace Barotrauma public bool ContainsKey(Identifier identifier) { Prefab.DisallowCallFromConstructor(); - return prefabs.ContainsKey(identifier); + return TryGet(identifier, out _); } public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier()); @@ -460,6 +490,19 @@ namespace Barotrauma topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null; OnSort?.Invoke(); HandleInheritance(this.Select(p => p.Identifier)); + + var enumerator = GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is IImplementsVariants implementsVariants && !IsInheritanceValid(p)) + { + DebugConsole.ThrowError( + $"Error in content package \"{p.ContentFile.ContentPackage.Name}\": " + + $"could not find the prefab \"{implementsVariants.VariantOf}\" the prefab \"{p.Identifier}\" is configured as a variant of."); + continue; + } + } } /// @@ -467,15 +510,19 @@ namespace Barotrauma /// /// IEnumerator public IEnumerator GetEnumerator() + { + return GetEnumerator(requireInheritanceValid: true); + } + + private IEnumerator GetEnumerator(bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - foreach (var kpv in prefabs) + foreach (var kvp in prefabs) { - var prefab = kpv.Value.ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) - { - yield return prefab; - } + var prefab = kvp.Value.ActivePrefab; + if (prefab == null || IsPrefabOverriddenByFile(prefab)) { continue; } + if (requireInheritanceValid && !IsInheritanceValid(prefab)) { continue; } + yield return prefab; } } @@ -485,7 +532,7 @@ namespace Barotrauma /// IEnumerator IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); + return GetEnumerator(requireInheritanceValid: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 62b8c8056..557860b69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -201,17 +201,28 @@ namespace Voronoi2 public bool IsPointInside(Vector2 point) { + if (!IsPointInsideAABB(point, margin: 0.0f)) { return false; } Vector2 transformedPoint = point - Translation; - if (Edges.All(e => e.Point1.X < transformedPoint.X && e.Point2.X < transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y < transformedPoint.Y && e.Point2.Y < transformedPoint.Y)) { return false; } - if (Edges.All(e => e.Point1.X > transformedPoint.X && e.Point2.X > transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y > transformedPoint.Y && e.Point2.Y > transformedPoint.Y)) { return false; } foreach (GraphEdge edge in Edges) { if (MathUtils.LinesIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } } return true; } + + public bool IsPointInsideAABB(Vector2 point2, float margin) + { + Vector2 transformedPoint = point2 - Translation; + Vector2 max = transformedPoint + Vector2.One * margin; + Vector2 min = transformedPoint - Vector2.One * margin; + + if (Edges.All(e => e.Point1.X < min.X && e.Point2.X < min.X)) { return false; } + if (Edges.All(e => e.Point1.Y < min.Y && e.Point2.Y < min.Y)) { return false; } + if (Edges.All(e => e.Point1.X > max.X && e.Point2.X > max.X)) { return false; } + if (Edges.All(e => e.Point1.Y > max.Y && e.Point2.Y > max.Y)) { return false; } + + return true; + } } public class GraphEdge diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 7b97635be..9b8bc37d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -61,10 +61,7 @@ namespace Barotrauma GameMain.GameSession?.CrewManager?.AutoShowCrewList(); #endif - foreach (MapEntity entity in MapEntity.mapEntityList) - { - entity.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if RUN_PHYSICS_IN_SEPARATE_THREAD var physicsThread = new Thread(ExecutePhysics) @@ -140,10 +137,7 @@ namespace Barotrauma { if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) { body.Update(); } } - foreach (MapEntity e in MapEntity.mapEntityList) - { - e.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index c78758bf3..5e5317022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -154,6 +154,7 @@ namespace Barotrauma { typeof(float), "float" }, { typeof(string), "string" }, { typeof(Identifier), "identifier" }, + { typeof(LanguageIdentifier), "languageidentifier" }, { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, @@ -240,7 +241,7 @@ namespace Barotrauma switch (typeName) { case "bool": - bool boolValue = value == "true" || value == "True"; + bool boolValue = value.ToIdentifier() == "true"; if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; @@ -290,6 +291,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, value.ToIdentifier()); break; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, value.ToLanguageIdentifier()); + break; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString(value)); break; @@ -373,6 +377,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, new Identifier((string)value)); return true; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, ((string)value).ToLanguageIdentifier()); + return true; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString((string)value)); return true; @@ -556,7 +563,7 @@ namespace Barotrauma public static string GetSupportedTypeName(Type type) { - if (type.IsEnum) return "Enum"; + if (type.IsEnum) { return "Enum"; } if (!supportedTypes.TryGetValue(type, out string typeName)) { return null; @@ -693,6 +700,29 @@ namespace Barotrauma case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { value = character.PropulsionSpeedMultiplier; return true; } } + break; + case nameof(Character.LowPassMultiplier): + { if (parentObject is Character character) { value = character.LowPassMultiplier; return true; } } + break; + case nameof(Character.HullOxygenPercentage): + { + if (parentObject is Character character) + { + value = character.HullOxygenPercentage; + return true; + } + else if (parentObject is Item item) + { + value = item.HullOxygenPercentage; + return true; + } + } + break; + case nameof(Door.Stuck): + { if (parentObject is Door door) { value = door.Stuck; return true; } } + break; } return false; } @@ -740,6 +770,23 @@ namespace Barotrauma case nameof(Controller.State): if (parentObject is Controller controller) { value = controller.State; return true; } break; + case nameof(Character.InWater): + { + if (parentObject is Character character) + { + value = character.InWater; + return true; + } + else if (parentObject is Item item) + { + value = item.InWater; + return true; + } + } + break; + case nameof(Rope.Snapped): + if (parentObject is Rope rope) { value = rope.Snapped; return true; } + break; } return false; } @@ -769,7 +816,7 @@ namespace Barotrauma switch (Name) { case nameof(Item.Condition): - if (parentObject is Item item) { item.Condition = value; return true; } + { if (parentObject is Item item) { item.Condition = value; return true; } } break; case nameof(Powered.Voltage): if (parentObject is Powered powered) { powered.Voltage = value; return true; } @@ -801,6 +848,9 @@ namespace Barotrauma case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } break; + case nameof(Item.Scale): + { if (parentObject is Item item) { item.Scale = value; return true; } } + break; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 9e662479a..ebd748ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -213,6 +213,16 @@ namespace Barotrauma return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, Identifier[] defaultValue, params string[] matchingAttributeName) + { + if (element == null) { return defaultValue; } + foreach (string name in matchingAttributeName) + { + var value = element.GetAttributeIdentifierArray(name, defaultValue); + if (value != defaultValue) { return value; } + } + return defaultValue; + } public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) { @@ -484,9 +494,18 @@ namespace Barotrauma { var attr = element?.GetAttribute(name); if (attr == null) { return defaultValue; } - return Enum.TryParse(attr.Value, true, out T result) ? result : - int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt) ? Unsafe.As(ref resultInt) : - defaultValue; + + if (Enum.TryParse(attr.Value, true, out T result)) + { + return result; + } + else if (int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt)) + { + return Unsafe.As(ref resultInt); + } + DebugConsole.ThrowError($"Error in {attr}! \"{attr}\" is not a valid {typeof(T).Name} value"); + return default; + } public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) @@ -608,10 +627,18 @@ namespace Barotrauma return mouseButton; } else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && - (Enum.GetValues(typeof(MouseButton)) as MouseButton[]).Contains((MouseButton)mouseButtonInt)) + Enum.GetValues().Contains((MouseButton)mouseButtonInt)) { return (MouseButton)mouseButtonInt; } + else if (string.Equals(strValue, "LeftMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.PrimaryMouse : MouseButton.SecondaryMouse; + } + else if (string.Equals(strValue, "RightMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.SecondaryMouse : MouseButton.PrimaryMouse; + } return defaultValue; } #endif @@ -807,7 +834,15 @@ namespace Barotrauma #endif return Color.White; } - + if (stringColor.StartsWith("faction.", StringComparison.OrdinalIgnoreCase)) + { + Identifier factionId = stringColor.Substring(8).ToIdentifier(); + if (FactionPrefab.Prefabs.TryGet(factionId, out var faction)) + { + return faction.IconColor; + } + return Color.White; + } string[] strComponents = stringColor.Split(','); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs index 5886c8ba3..617058963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs @@ -1,13 +1,134 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.IO; +using XmlWriterSettings = System.Xml.XmlWriterSettings; +#nullable enable namespace Barotrauma { - public class CreatureMetrics + public static class CreatureMetrics { - public readonly HashSet RecentlyEncountered = new HashSet(); - public readonly HashSet Encountered = new HashSet(); - public readonly HashSet Killed = new HashSet(); + private const string path = "creature_metrics.xml"; - public readonly static CreatureMetrics Instance = new CreatureMetrics(); + /// + /// Resets every round. + /// + public static HashSet RecentlyEncountered { get; private set; } = new HashSet(); + public static HashSet Encountered { get; private set; } = new HashSet(); + public static HashSet Unlocked { get; private set; } = new HashSet(); + public static HashSet Killed { get; private set; } = new HashSet(); + public static bool IsInitialized { get; private set; } + public static bool UnlockAll { get; set; } + + public static void Init() + { + IsInitialized = true; + if (File.Exists(path)) + { + Load(); + } + Save(); + } + + private static void Load() + { + XDocument doc = XMLExtensions.TryLoadXml(path); + XElement? root = doc?.Root; + if (root == null) + { + DebugConsole.AddWarning($"Failed to load creature metrics from {path}!"); + return; + } + UnlockAll = root.GetAttributeBool(nameof(UnlockAll), UnlockAll); + Unlocked = new HashSet(root.GetAttributeIdentifierArray(nameof(Unlocked), Array.Empty())); + Encountered = new HashSet(root.GetAttributeIdentifierArray(nameof(Encountered), Array.Empty())); + Killed = new HashSet(root.GetAttributeIdentifierArray(nameof(Killed), Array.Empty())); + SyncSets(); + } + + public static void Save() + { + if (!IsInitialized) + { + throw new Exception("Creature Metrics not yet initialized!"); + } + SyncSets(); + XDocument configDoc = new XDocument(); + XElement root = new XElement("CreatureMetrics"); + configDoc.Add(root); + root.SetAttributeValue(nameof(UnlockAll), UnlockAll); + root.SetAttributeValue(nameof(Unlocked), string.Join(",", Unlocked).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Encountered), string.Join(",", Encountered).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Killed), string.Join(",", Killed).Trim().ToLowerInvariant()); + configDoc.SaveSafe(path); + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + try + { + using var writer = XmlWriter.Create(path, settings); + configDoc.WriteTo(writer); + writer.Flush(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving creature metrics failed.", e); + GameAnalyticsManager.AddErrorEventOnce("CreatureMetrics.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, + "Saving creature metrics failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } + } + + public static void RecordKill(Identifier species) + { + AddEncounter(species); + if (!Killed.Contains(species)) + { + Killed.Add(species); + } + } + + public static void AddEncounter(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Encountered.Contains(species)) { return; } + Encountered.Add(species); + RecentlyEncountered.Add(species); + UnlockInEditor(species); + } + + private static IEnumerable? vanillaCharacters; + public static void UnlockInEditor(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Unlocked.Contains(species)) { return; } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); + var contentFile = CharacterPrefab.FindBySpeciesName(species); + if (contentFile == null) { return; } + if (!vanillaCharacters.Contains(contentFile.ContentFile)) + { + // Don't try to unlock custom characters. They are always unlocked. + return; + } + Unlocked.Add(species); + } + + private static void SyncSets() + { + // Ensure that all killed are also encountered and both unlocked. + // Otherwise we could permanently hide some creatures by manually adding them to the encountered or by removing from unlocked in the xml file. + foreach (var species in Killed) + { + Encountered.Add(species); + } + foreach (var species in Encountered) + { + Unlocked.Add(species); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 5776bc9e0..2e3444126 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -25,7 +25,8 @@ namespace Barotrauma { None = 0, Transparent = 1, - Opaque = 2 + Opaque = 2, + BlockOutsideView = 3 } public enum VoiceMode @@ -110,6 +111,7 @@ namespace Barotrauma #if CLIENT retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); + retVal.SavedCampaignSettings = element.GetChildElement("campaignsettings"); LoadSubEditorImages(element); #endif @@ -139,6 +141,9 @@ namespace Barotrauma public bool DisableInGameHints; public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; +#if CLIENT + public XElement SavedCampaignSettings; +#endif #if DEBUG public bool UseSteamMatchmaking; public bool RequireSteamAuthentication; @@ -230,7 +235,7 @@ namespace Barotrauma SoundVolume = 0.5f, UiVolume = 0.3f, VoiceChatVolume = 0.5f, - VoiceChatCutoffPrevention = 0, + VoiceChatCutoffPrevention = 200, MicrophoneVolume = 5, MuteOnFocusLost = false, DynamicRangeCompressionEnabled = true, @@ -618,6 +623,8 @@ namespace Barotrauma root.Add(inventoryKeyMappingElement); SubEditorScreen.ImageManager.Save(root); + + root.Add(CampaignSettings.CurrentSettings.Save()); #endif configDoc.SaveSafe(PlayerConfigPath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs new file mode 100644 index 000000000..55c5892e3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma; + +static class ServerLanguageOptions +{ + public readonly record struct LanguageOption( + string Label, + LanguageIdentifier Identifier, + ImmutableArray MapsFrom) + { + public static LanguageOption FromXElement(XElement element) + => new LanguageOption( + Label: + element.GetAttributeString("label", ""), + Identifier: + element.GetAttributeIdentifier("identifier", LanguageIdentifier.None.Value) + .ToLanguageIdentifier(), + MapsFrom: + element.GetAttributeIdentifierArray("mapsFrom", Array.Empty()) + .Select(id => id.ToLanguageIdentifier()).ToImmutableArray()); + } + + public static readonly ImmutableArray Options; + + static ServerLanguageOptions() + { + var languageOptionElements + = XMLExtensions.TryLoadXml("Data/languageoptions.xml")?.Root?.Elements() + ?? Enumerable.Empty(); + Options = languageOptionElements + // Convert the XElements into LanguageOptions immediately since they can be worked with more directly + .Select(LanguageOption.FromXElement) + // Remove options with duplicate identifiers + .DistinctBy(p => p.Identifier) + // Remove options where the label is empty or the identifier is missing + .Where(p => !p.Label.IsNullOrWhiteSpace() && p.Identifier != LanguageIdentifier.None) + // Sort the options based on the lexicographical order of the labels + .OrderBy(p => p.Label) + .ToImmutableArray(); + } + + public static LanguageIdentifier PickLanguage(LanguageIdentifier id) + { + if (id == LanguageIdentifier.None) + { + id = GameSettings.CurrentConfig.Language; + } + + foreach (var (_, identifier, mapsFrom) in Options) + { + if (id == identifier || mapsFrom.Contains(id)) + { + return identifier; + } + } + + return TextManager.DefaultLanguage; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index 9cc87f0cc..d2e3f5467 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Xml.Linq; using System.Linq; -using System; namespace Barotrauma { @@ -10,7 +8,7 @@ namespace Barotrauma public readonly List conditionals = new List(); public bool IsActive { get; private set; } = true; - public readonly PropertyConditional.Comparison Comparison; + public readonly PropertyConditional.LogicalOperatorType LogicalOperator; public readonly bool Exclusive; public ISerializableEntity Target { get; private set; } public Sprite Sprite { get; private set; } @@ -21,23 +19,14 @@ namespace Barotrauma { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); - string comparison = element.GetAttributeString("comparison", null); - if (comparison != null) - { - Enum.TryParse(comparison, ignoreCase: true, out Comparison); - } + LogicalOperator = element.GetAttributeEnum(nameof(LogicalOperator), + element.GetAttributeEnum("comparison", LogicalOperator)); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "sprite": Sprite = new Sprite(subElement, file: file, lazyLoad: lazyLoad); @@ -57,7 +46,7 @@ namespace Barotrauma } else { - IsActive = Comparison == PropertyConditional.Comparison.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); + IsActive = LogicalOperator == PropertyConditional.LogicalOperatorType.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 60b0aca6d..b8e5e84f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -39,16 +39,10 @@ namespace Barotrauma public DelayedEffect(ContentXElement element, string parentDebugName) : base(element, parentDebugName) { - string delayTypeStr = element.GetAttributeString("delaytype", "timer"); - if (!Enum.TryParse(typeof(DelayTypes), delayTypeStr, ignoreCase: true, out var delayType)) + DelayTypes delayTypeAttr = element.GetAttributeEnum("delaytype", DelayTypes.Timer); + if (delayTypeAttr is DelayTypes.Timer) { - DebugConsole.ThrowError("Invalid delay type \"" + delayTypeStr + "\" in StatusEffect (" + parentDebugName + ")"); - } - switch (delayType) - { - case DelayTypes.Timer: - delay = element.GetAttributeFloat("delay", 1.0f); - break; + delay = element.GetAttributeFloat("delay", 1.0f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index b241816f3..cd9a41b1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,71 +1,246 @@ -using System; +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - // TODO: This class should be refactored: - // - Use XElement instead of XAttribute in the constructor - // - Simplify, remove unnecessary conversions - // - Improve the flow so that the logic is undestandable. - // - Maybe add some test cases for the operators? - class PropertyConditional + /// + /// Conditionals are used by some in-game mechanics to require one + /// or more conditions to be met for those mechanics to be active. + /// For example, some StatusEffects use Conditionals to only trigger + /// if the affected character is alive. + /// + sealed class PropertyConditional { + // TODO: Make this testable and add tests + + /// + /// Category of properties to check against + /// public enum ConditionType { - Uncertain, - PropertyValue, + /// + /// Depending on what's available, check against either one + /// of the target object's properties or the strength of an + /// affliction. + /// + /// The target object's available properties depend on how that + /// object is defined in the [source code](https://github.com/Regalis11/Barotrauma). + /// + /// This is not applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + PropertyValueOrAffliction, + + /// + /// Check against the target character's skill with the same name as the attribute. + /// + /// This is only applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + SkillRequirement, + + /// + /// Check against the name of the target. + /// Name, + + /// + /// Check against the species identifier of the target. Only works on characters. + /// SpeciesName, + + /// + /// Check against the species group of the target. Only works on characters. + /// SpeciesGroup, + + /// + /// Check against the target's tags. Only works on items. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasTag, + + /// + /// Check against the tags of the target's active status effects. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasStatusTag, + + /// + /// Check against the target's specifier tags. In the vanilla game, these are the head index + /// and gender. See human.xml for more details. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasSpecifierTag, - Affliction, + + /// + /// Check against the target's entity type. + /// + /// The currently supported values are "character", "limb", "item", "structure" and "null". + /// EntityType, - LimbType, - SkillRequirement + + /// + /// Check against the target's limb type. See . + /// + LimbType } - public enum Comparison + public enum LogicalOperatorType { And, Or } - public enum OperatorType + /// + /// There are several ways to compare properties to values. The comparison operator + /// to use can be specified by placing one of the following before the value to compare + /// against. + /// + public enum ComparisonOperatorType { None, + + /// + /// Require that the property being checked equals the given value. + /// + /// This is the default operator used if none is specified. + /// Equals, + + /// + /// Require that the property being checked doesn't equal the given value. + /// NotEquals, + + /// + /// Require that the property being checked is less than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThan, + + /// + /// Require that the property being checked is less than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThanEquals, + + /// + /// Require that the property being checked is greater than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThan, + + /// + /// Require that the property being checked is greater than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThanEquals } public readonly ConditionType Type; - public readonly OperatorType Operator; + public readonly ComparisonOperatorType ComparisonOperator; public readonly Identifier AttributeName; public readonly string AttributeValue; - public readonly string[] SplitAttributeValue; + public readonly ImmutableArray AttributeValueAsTags; public readonly float? FloatValue; - public readonly string TargetItemComponentName; + /// + /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. + /// Only works on items. + /// + public readonly string TargetItemComponent; - // Only used by attacks + /// + /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character + /// public readonly bool TargetSelf; - // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) + /// + /// If set to true, the conditionals defined by this element check against the entity containing the target. + /// public readonly bool TargetContainer; - // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. + + /// + /// If this and TargetContainer are set to true, the conditionals defined by this element check against the entity containing the target's container. + /// public readonly bool TargetGrandParent; + /// + /// If set to true, the conditionals defined by this element check against the items contained by the target. Only works with items. + /// public readonly bool TargetContainedItem; - // Remove this after refactoring - public static bool IsValid(XAttribute attribute) + public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) + { + var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); + var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); + var targetSelf = element.GetAttributeBool(nameof(TargetSelf), false); + var targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false); + var targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false); + + ConditionType? overrideConditionType = null; + if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false)) + { + overrideConditionType = ConditionType.SkillRequirement; + } + + foreach (var attribute in element.Attributes()) + { + if (!IsValid(attribute)) { continue; } + if (predicate != null && !predicate(attribute)) { continue; } + + var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); + if (string.IsNullOrWhiteSpace(attributeValueString)) + { + DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); + continue; + } + + var conditionType = overrideConditionType ?? + (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) + ? type + : ConditionType.PropertyValueOrAffliction); + + yield return new PropertyConditional( + attributeName: attribute.NameAsIdentifier(), + comparisonOperator: comparisonOperator, + attributeValue: attributeValueString, + targetItemComponent: targetItemComponent, + targetSelf: targetSelf, + targetContainer: targetContainer, + targetGrandParent: targetGrandParent, + targetContainedItem: targetContainedItem, + conditionType: conditionType); + } + } + + private static bool IsValid(XAttribute attribute) { switch (attribute.Name.ToString().ToLowerInvariant()) { @@ -82,60 +257,63 @@ namespace Barotrauma } } - // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) - public PropertyConditional(XAttribute attribute) + private PropertyConditional( + Identifier attributeName, + ComparisonOperatorType comparisonOperator, + string attributeValue, + string targetItemComponent, + bool targetSelf, + bool targetContainer, + bool targetGrandParent, + bool targetContainedItem, + ConditionType conditionType) { - AttributeName = attribute.NameAsIdentifier(); - string attributeValueString = attribute.Value; - if (string.IsNullOrWhiteSpace(attributeValueString)) - { - DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); - return; - } - string valueString = attributeValueString; - string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } - Operator = GetOperatorType(splitString[0]); + AttributeName = attributeName; - if (Operator == OperatorType.None) - { - Operator = OperatorType.Equals; - valueString = attributeValueString; - } + TargetItemComponent = targetItemComponent; + TargetSelf = targetSelf; + TargetContainer = targetContainer; + TargetGrandParent = targetGrandParent; + TargetContainedItem = targetContainedItem; - TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); - TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); - TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); - TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); - TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); + Type = conditionType; - if (!Enum.TryParse(AttributeName.Value, true, out Type)) - { - Type = ConditionType.Uncertain; - } - - if (attribute.Parent.GetAttributeBool("skillrequirement", false)) - { - Type = ConditionType.SkillRequirement; - } - - AttributeValue = valueString; - SplitAttributeValue = valueString.Split(','); + ComparisonOperator = comparisonOperator; + AttributeValue = attributeValue; + AttributeValueAsTags = AttributeValue.Split(',') + //, options: StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => s.ToIdentifier()) + .ToImmutableArray(); if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { FloatValue = value; } } - public static OperatorType GetOperatorType(string op) + public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) + { + str ??= ""; + + ComparisonOperatorType op = ComparisonOperatorType.Equals; + string conditionStr = str; + if (str.IndexOf(' ') is var i and >= 0) + { + op = GetComparisonOperatorType(str[..i]); + if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; } + else { op = ComparisonOperatorType.Equals; } + } + return (op, conditionStr); + } + + public static ComparisonOperatorType GetComparisonOperatorType(string op) { //thanks xml for not letting me use < or > in attributes :( - switch (op) + switch (op.ToLowerInvariant()) { case "e": case "eq": case "equals": - return OperatorType.Equals; + return ComparisonOperatorType.Equals; case "ne": case "neq": case "notequals": @@ -143,311 +321,280 @@ namespace Barotrauma case "!e": case "!eq": case "!equals": - return OperatorType.NotEquals; + return ComparisonOperatorType.NotEquals; case "gt": case "greaterthan": - return OperatorType.GreaterThan; + return ComparisonOperatorType.GreaterThan; case "lt": case "lessthan": - return OperatorType.LessThan; + return ComparisonOperatorType.LessThan; case "gte": case "gteq": case "greaterthanequals": - return OperatorType.GreaterThanEquals; + return ComparisonOperatorType.GreaterThanEquals; case "lte": case "lteq": case "lessthanequals": - return OperatorType.LessThanEquals; + return ComparisonOperatorType.LessThanEquals; default: - return OperatorType.None; + return ComparisonOperatorType.None; } } + private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals; - public bool Matches(ISerializableEntity target) + public bool Matches(ISerializableEntity? target) { - return Matches(target, TargetContainedItem); + return TargetContainedItem + ? MatchesContained(target) + : MatchesDirect(target); } - public bool Matches(ISerializableEntity target, bool checkContained) + private bool MatchesContained(ISerializableEntity? target) { - var type = Type; - if (type == ConditionType.Uncertain) + var containedItems = target switch { - type = AfflictionPrefab.Prefabs.ContainsKey(AttributeName) - ? ConditionType.Affliction - : ConditionType.PropertyValue; - } - - if (checkContained) + Item item + => item.ContainedItems, + ItemComponent ic + => ic.Item.ContainedItems, + Character {Inventory: { } characterInventory} + => characterInventory.AllItems, + _ + => Enumerable.Empty() + }; + foreach (var containedItem in containedItems) { - if (target is Item item) - { - foreach (var containedItem in item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Items.Components.ItemComponent ic) - { - foreach (var containedItem in ic.Item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Character character) - { - if (character.Inventory == null) { return false; } - foreach (var containedItem in character.Inventory.AllItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } + if (MatchesDirect(containedItem)) { return true; } } + return false; + } - switch (type) + private bool MatchesDirect(ISerializableEntity? target) + { + Character? targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + switch (Type) { - case ConditionType.PropertyValue: - SerializableProperty property; - if (target?.SerializableProperties == null) { return Operator == OperatorType.NotEquals; } - if (target.SerializableProperties.TryGetValue(AttributeName, out property)) + case ConditionType.PropertyValueOrAffliction: + // First try checking for a property belonging to the target + if (target?.SerializableProperties != null + && target.SerializableProperties.TryGetValue(AttributeName, out var property)) { - return Matches(target, property); + return PropertyMatchesRequirement(target, property); } - return false; - case ConditionType.Name: - if (target == null) { return Operator == OperatorType.NotEquals; } - return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); + // Then try checking for an affliction affecting the target + if (targetChar is { CharacterHealth: { } health }) + { + var affliction = health.GetAffliction(AttributeName.ToIdentifier()); + float afflictionStrength = affliction?.Strength ?? 0f; + + return NumberMatchesRequirement(afflictionStrength); + } + return ComparisonOperatorIsNotEquals; + case ConditionType.SkillRequirement: + if (targetChar != null) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return NumberMatchesRequirement(skillLevel); + } + return ComparisonOperatorIsNotEquals; case ConditionType.HasTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - return MatchesTagCondition(target); + return ItemMatchesTagCondition(target); case ConditionType.HasStatusTag: - if (target == null) { return Operator == OperatorType.NotEquals; } + if (target == null) { return ComparisonOperatorIsNotEquals; } + + // TODO: revisit this. As written, the current behavior is: + // - ComparisonOperatorType.Equals: true when any effects have all tags + // - ComparisonOperatorType.NotEquals: true when none of the effects have any of the tags int matches = 0; - foreach (DurationListElement durationEffect in StatusEffect.DurationList) + + foreach (var durationEffect in StatusEffect.DurationList) { if (!durationEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (durationEffect.Parent.HasTag(tag)) - { - matches++; - } - } + if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { matches++; } } - foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) + + foreach (var delayedEffect in DelayedEffect.DelayList) { if (!delayedEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (delayedEffect.Parent.HasTag(tag)) - { - matches++; - } - } + if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { matches++; } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; - case ConditionType.HasSpecifierTag: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character { Info: { } characterInfo })) { return false; } - return (Operator == OperatorType.Equals) == - SplitAttributeValue.All(v => characterInfo.Head.Preset.TagSet.Contains(v)); - } - case ConditionType.SpeciesName: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); - } - case ConditionType.SpeciesGroup: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == targetCharacter.Params.CompareGroup(AttributeValue.ToIdentifier()); - } - case ConditionType.EntityType: - switch (AttributeValue) - { - case "character": - case "Character": - return (Operator == OperatorType.Equals) == target is Character; - case "limb": - case "Limb": - return (Operator == OperatorType.Equals) == target is Limb; - case "item": - case "Item": - return (Operator == OperatorType.Equals) == target is Item; - case "structure": - case "Structure": - return (Operator == OperatorType.Equals) == target is Structure; - case "null": - return (Operator == OperatorType.Equals) == (target == null); - default: - return false; - } - case ConditionType.LimbType: - { - if (!(target is Limb limb)) - { - return false; - } - else - { - return limb.type.ToString().Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); - } - } - case ConditionType.Affliction: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - Character targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - if (targetChar != null) - { - var health = targetChar.CharacterHealth; - if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName.ToIdentifier()); - float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - - return ValueMatchesRequirement(afflictionStrength); - } - } - return false; - case ConditionType.SkillRequirement: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - if (target is Character targetChar) - { - float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); - - return ValueMatchesRequirement(skillLevel); - } - } - return false; + return ComparisonOperatorIsNotEquals + ? matches >= StatusEffect.DurationList.Count + DelayedEffect.DelayList.Count + : matches > 0; default: - return false; + bool equals = CheckOnlyEquality(target); + return ComparisonOperatorIsNotEquals + ? !equals + : equals; } } - private bool ValueMatchesRequirement(float testedValue) + private bool CheckOnlyEquality(ISerializableEntity? target) { - if (FloatValue.HasValue) + switch (Type) { - float value = FloatValue.Value; - switch (Operator) + case ConditionType.Name: + if (target == null) { return false; } + + return target.Name == AttributeValue; + case ConditionType.HasSpecifierTag: { - case OperatorType.Equals: - return testedValue == value; - case OperatorType.GreaterThan: - return testedValue > value; - case OperatorType.GreaterThanEquals: - return testedValue >= value; - case OperatorType.LessThan: - return testedValue < value; - case OperatorType.LessThanEquals: - return testedValue <= value; - case OperatorType.NotEquals: - return testedValue != value; + if (target is not Character {Info: { } characterInfo}) + { + return false; + } + + return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains); + } + case ConditionType.SpeciesName: + { + if (target is not Character targetCharacter) + { + return false; + } + + return targetCharacter.SpeciesName == AttributeValue; + } + case ConditionType.SpeciesGroup: + { + if (target is not Character targetCharacter) + { + return false; + } + + return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group); + } + case ConditionType.EntityType: + return AttributeValue.ToLowerInvariant() switch + { + "character" + => target is Character, + "limb" + => target is Limb, + "item" + => target is Item, + "structure" + => target is Structure, + "null" + => target == null, + _ + => false + }; + case ConditionType.LimbType: + { + return target is Limb limb + && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType) + && attributeLimbType == limb.type; } } return false; } - private bool MatchesTagCondition(ISerializableEntity target) + private bool SufficientTagMatches(int matches) { - if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } - - int matches = 0; - foreach (string tag in SplitAttributeValue) - { - if (item.HasTag(tag)) - { - matches++; - } - } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return ComparisonOperatorIsNotEquals + ? matches <= 0 + : matches >= AttributeValueAsTags.Length; } - public bool MatchesTagCondition(Identifier targetTag) + private bool ItemMatchesTagCondition(ISerializableEntity? target) + { + if (target is not Item item) { return ComparisonOperatorIsNotEquals; } + + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (item.HasTag(tag)) { matches++; } + } + return SufficientTagMatches(matches); + } + + public bool TargetTagMatchesTagCondition(Identifier targetTag) { if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; - foreach (string tag in SplitAttributeValue) + foreach (var tag in AttributeValueAsTags) { - if (targetTag == tag) - { - matches++; - } + if (targetTag == tag) { matches++; } } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return SufficientTagMatches(matches); } - // TODO: refactor and add tests - private bool Matches(ISerializableEntity target, SerializableProperty property) + private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) + { + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (statusEffect.HasTag(tag.Value)) { matches++; } + } + return SufficientTagMatches(matches); + } + + private bool NumberMatchesRequirement(float testedValue) + { + if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } + float value = FloatValue.Value; + + return ComparisonOperator switch + { + ComparisonOperatorType.Equals + => MathUtils.NearlyEqual(testedValue, value), + ComparisonOperatorType.NotEquals + => !MathUtils.NearlyEqual(testedValue, value), + ComparisonOperatorType.GreaterThan + => testedValue > value, + ComparisonOperatorType.GreaterThanEquals + => testedValue >= value, + ComparisonOperatorType.LessThan + => testedValue < value, + ComparisonOperatorType.LessThanEquals + => testedValue <= value, + _ + => false + }; + } + + private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property) { Type type = property.PropertyType; if (type == typeof(float) || type == typeof(int)) { float floatValue = property.GetFloatValue(target); - switch (Operator) - { - case OperatorType.Equals: - return MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.NotEquals: - return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.GreaterThan: - return floatValue > FloatValue.Value; - case OperatorType.LessThan: - return floatValue < FloatValue.Value; - case OperatorType.GreaterThanEquals: - return floatValue >= FloatValue.Value; - case OperatorType.LessThanEquals: - return floatValue <= FloatValue.Value; - } - return false; + return NumberMatchesRequirement(floatValue); } - switch (Operator) + switch (ComparisonOperator) { - case OperatorType.Equals: + case ComparisonOperatorType.Equals: + case ComparisonOperatorType.NotEquals: + bool equals; + if (type == typeof(bool)) { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); - } - var value = property.GetValue(target); - return Equals(value, AttributeValue); + bool attributeValueBool = AttributeValue.IsTrueString(); + equals = property.GetBoolValue(target) == attributeValueBool; } - case OperatorType.NotEquals: + else { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); - } var value = property.GetValue(target); - return !Equals(value, AttributeValue); + equals = AreValuesEquivalent(value, AttributeValue); } - case OperatorType.GreaterThan: - case OperatorType.LessThanEquals: - case OperatorType.LessThan: - case OperatorType.GreaterThanEquals: + + return ComparisonOperatorIsNotEquals + ? !equals + : equals; + default: DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + "Make sure the type of the value set in the config files matches the type of the property."); - break; + return false; } - return false; - static bool Equals(object value, string desiredValue) + static bool AreValuesEquivalent(object? value, string desiredValue) { if (value == null) { @@ -455,7 +602,7 @@ namespace Barotrauma } else { - return value.ToString().Equals(desiredValue); + return (value.ToString() ?? "").Equals(desiredValue); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 9f3d59506..292976e65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -42,129 +42,183 @@ namespace Barotrauma } } - class AITrigger : ISerializableEntity - { - public string Name => "ai trigger"; - - public Dictionary SerializableProperties { get; set; } - - [Serialize(AIState.Idle, IsPropertySaveable.No)] - public AIState State { get; private set; } - - [Serialize(0f, IsPropertySaveable.No)] - public float Duration { get; private set; } - - [Serialize(1f, IsPropertySaveable.No)] - public float Probability { get; private set; } - - [Serialize(0f, IsPropertySaveable.No)] - public float MinDamage { get; private set; } - - [Serialize(true, IsPropertySaveable.No)] - public bool AllowToOverride { get; private set; } - - [Serialize(true, IsPropertySaveable.No)] - public bool AllowToBeOverridden { get; private set; } - - public bool IsTriggered { get; private set; } - - public float Timer { get; private set; } - - public bool IsActive { get; private set; } - - public bool IsPermanent { get; private set; } - - public void Launch() - { - IsTriggered = true; - IsActive = true; - IsPermanent = Duration <= 0; - if (!IsPermanent) - { - Timer = Duration; - } - } - - public void Reset() - { - IsTriggered = false; - IsActive = false; - Timer = 0; - } - - public void UpdateTimer(float deltaTime) - { - if (IsPermanent) { return; } - Timer -= deltaTime; - if (Timer < 0) - { - Timer = 0; - IsActive = false; - } - } - - public AITrigger(XElement element) - { - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - } - } - + /// + /// StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in some way, spawning things, playing sounds, + /// emitting particles, creating fire and explosions, increasing a characters' skill. They are a crucial part of modding Barotrauma: all kinds of + /// custom behaviors of an item or a creature for example are generally created using StatusEffects. + /// + /// + /// + /// Can be used to delay the execution of the effect. For example, you could have an effect that triggers when a character receives damage, + /// but takes 5 seconds before it starts to do anything. + /// + /// + /// An arbitrary tag (or a list of tags) that describe the status effect and can be used by Conditionals to check whether some StatusEffect is running. + /// For example, an item could execute a StatusEffect with the tag "poisoned" on some character, and the character could have an effect that makes + /// the character do something when an effect with that tag is active. + /// + /// + /// And/Or. Do all of the Conditionals defined in the effect be true for the effect to execute, or should the effect execute when any of them is true? + /// + /// + /// These are the meat of the StatusEffects. You can set, increment or decrement any value of the target, be it an item, character, limb or hull. + /// By default, the value is added to the existing value. If you want to instead set the value, use the setValue attribute. + /// For example, Condition="-5" would decrease the condition of the item the effect is targeting by 5 per second. If the target has no property + /// with the specified name, the attribute does nothing. + /// + /// partial class StatusEffect { + private static readonly ImmutableHashSet FieldNames; + static StatusEffect() + { + FieldNames = typeof(StatusEffect).GetFields().AsEnumerable().Select(f => f.Name.ToIdentifier()).ToImmutableHashSet(); + } + [Flags] public enum TargetType { + /// + /// The entity (item, character, limb) the StatusEffect is defined in. + /// This = 1, + /// + /// In the context of items, the container the item is inside (if any). In the context of limbs, the character the limb belongs to. + /// Parent = 2, + /// + /// The character the StatusEffect is defined in. In the context of items and attacks, the character using the item/attack. + /// Character = 4, + /// + /// The item(s) contained in the inventory of the entity the StatusEffect is defined in. + /// Contained = 8, + /// + /// Characters near the entity the StatusEffect is defined in. The range is defined using . + /// NearbyCharacters = 16, + /// + /// Items near the entity the StatusEffect is defined in. The range is defined using . + /// NearbyItems = 32, + /// + /// The entity the item/attack is being used on. + /// UseTarget = 64, + /// + /// The hull the entity is inside. + /// Hull = 128, + /// + /// The entity the item/attack is being used on. In the context of characters, one of the character's limbs (specify which one using ). + /// Limb = 256, + /// + /// All limbs of the character the effect is being used on. + /// AllLimbs = 512, + /// + /// Last limb of the character the effect is being used on. + /// LastLimb = 1024 } + /// + /// Defines items spawned by the effect, and where and how they're spawned. + /// class ItemSpawnInfo { public enum SpawnPositionType { + /// + /// The position of the StatusEffect's target. + /// This, - //the inventory of the StatusEffect's target entity + /// + /// The inventory of the StatusEffect's target. + /// ThisInventory, - //the same inventory the StatusEffect's target entity is in (only valid if the target is an Item) + /// + /// The same inventory the StatusEffect's target entity is in. Only valid if the target is an Item. + /// SameInventory, - //the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) + /// + /// The inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) + /// ContainedInventory } public enum SpawnRotationType { + /// + /// Fixed rotation specified using the Rotation attribute. + /// Fixed, + /// + /// The rotation of the entity executing the StatusEffect + /// Target, + /// + /// The rotation of the limb executing the StatusEffect, or the limb the StatusEffect is targeting + /// Limb, + /// + /// The rotation of the main limb (usually torso) of the character executing the StatusEffect + /// MainLimb, + /// + /// The rotation of the collider of the character executing the StatusEffect + /// Collider, + /// + /// Random rotation between 0 and 360 degrees. + /// Random } public readonly ItemPrefab ItemPrefab; + /// + /// Where should the item spawn? + /// public readonly SpawnPositionType SpawnPosition; + + /// + /// Should the item spawn even if the container is already full? + /// public readonly bool SpawnIfInventoryFull; /// - /// Should the item spawn even if the container can't contain items of this type + /// Should the item spawn even if the container can't contain items of this type or if it's already full? /// public readonly bool SpawnIfCantBeContained; + /// + /// Impulse applied to the item when it spawns (i.e. how fast the item launched off). + /// public readonly float Impulse; public readonly float RotationRad; + /// + /// How many items to spawn. + /// public readonly int Count; + /// + /// Random offset added to the spawn position in pixels. + /// public readonly float Spread; + /// + /// What should the initial rotation of the item be? + /// public readonly SpawnRotationType RotationType; + /// + /// Amount of random variance in the initial rotation of the item (in degrees). + /// public readonly float AimSpreadRad; + /// + /// Should the item be automatically equipped when it spawns? Only valid if the item spawns in a character's inventory. + /// public readonly bool Equip; - + /// + /// Condition of the item when it spawns (1.0 = max). + /// public readonly float Condition; public ItemSpawnInfo(XElement element, string parentDebugName) @@ -208,19 +262,19 @@ namespace Barotrauma AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f)); Equip = element.GetAttributeBool("equip", false); - string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); - if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position."); - } - string rotationTypeStr = element.GetAttributeString("rotationtype", RotationRad != 0 ? "Fixed" : "Target"); - if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type."); - } + SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); + RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); } } + /// + /// Can be used by to check whether some specific StatusEffect is running. + /// + /// + /// + /// An arbitrary identifier the Ability can check for. + /// + /// public class AbilityStatusEffectIdentifier : AbilityObject { public AbilityStatusEffectIdentifier(Identifier effectIdentifier) @@ -230,9 +284,18 @@ namespace Barotrauma public Identifier EffectIdentifier { get; set; } } + /// + /// Unlocks a talent, or multiple talents when the effect executes. Only valid if the target is a character or a limb. + /// public class GiveTalentInfo { + /// + /// The identifier(s) of the talents that should be unlocked. + /// public Identifier[] TalentIdentifiers; + /// + /// If true and there's multiple identifiers defined, a random one will be chosen instead of unlocking all of them. + /// public bool GiveRandom; public GiveTalentInfo(XElement element, string _) @@ -242,10 +305,22 @@ namespace Barotrauma } } + /// + /// Increases a character's skills when the effect executes. Only valid if the target is a character or a limb. + /// public class GiveSkill { + /// + /// The identifier of the skill to increase. + /// public readonly Identifier SkillIdentifier; + /// + /// How much to increase the skill. + /// public readonly float Amount; + /// + /// Should the talents that trigger when the character gains skills be triggered by the effect? + /// public readonly bool TriggerTalents; public GiveSkill(XElement element, string parentDebugName) @@ -261,52 +336,66 @@ namespace Barotrauma } } + /// + /// Defines characters spawned by the effect, and where and how they're spawned. + /// public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; public Dictionary SerializableProperties { get; set; } - [Serialize(false, IsPropertySaveable.No)] - public bool TransferBuffs { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool TransferAfflictions { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool TransferInventory { get; private set; } - - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "The species name (identifier) of the character to spawn.")] public Identifier SpeciesName { get; private set; } - [Serialize(1, IsPropertySaveable.No)] + [Serialize(1, IsPropertySaveable.No, description: "How many characters to spawn.")] public int Count { get; private set; } - /// - /// The maximum amount of creatures of the same species in the same team that are allowed to be spawned via this status effect. - /// Also the creatures spawned by other means are counted in the check. - /// - [Serialize(0, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the buffs of the character executing the effect be transferred to the spawned character?"+ + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferBuffs { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: + "Should the afflictions of the character executing the effect be transferred to the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferAfflictions { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: + "Should the the items from the character executing the effect be transferred to the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferInventory { get; private set; } + + [Serialize(0, IsPropertySaveable.No, description: + "The maximum number of creatures of the given species and team that can exist in the current level before this status effect stops spawning any more.")] public int TotalMaxCount { get; private set; } - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "Amount of stun to apply on the spawned character.")] public int Stun { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "An affliction to apply on the spawned character.")] public Identifier AfflictionOnSpawn { get; private set; } - [Serialize(1, IsPropertySaveable.No)] + [Serialize(1, IsPropertySaveable.No, description: + $"The strength of the affliction applied on the spawned character. Only relevant if {nameof(AfflictionOnSpawn)} is defined.")] public int AfflictionStrength { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the player controlling the character that executes the effect gain control of the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] public bool TransferControl { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the character that executes the effect be removed when the effect executes?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] public bool RemovePreviousCharacter { get; private set; } - [Serialize(0f, IsPropertySaveable.No)] + [Serialize(0f, IsPropertySaveable.No, description: "Amount of random spread to add to the spawn position. " + + "Can be used to prevent all the characters from spawning at the exact same position if the effect spawns multiple ones.")] public float Spread { get; private set; } - [Serialize("0,0", IsPropertySaveable.No)] + [Serialize("0,0", IsPropertySaveable.No, description: + "Offset added to the spawn position. " + + "Can be used to for example spawn a character a bit up from the center of an item executing the effect.")] public Vector2 Offset { get; private set; } public CharacterSpawnInfo(XElement element, string parentDebugName) @@ -319,43 +408,142 @@ namespace Barotrauma } } + /// + /// Can be used to trigger a behavior change of some kind on an AI character. Only applicable for enemy characters, not humans. + /// + public class AITrigger : ISerializableEntity + { + public string Name => "ai trigger"; + + public Dictionary SerializableProperties { get; set; } + + [Serialize(AIState.Idle, IsPropertySaveable.No, description: "The AI state the character should switch to.")] + public AIState State { get; private set; } + + [Serialize(0f, IsPropertySaveable.No, description: "How long should the character stay in the specified state? If 0, the effect is permanent (unless overridden by another AITrigger).")] + public float Duration { get; private set; } + + [Serialize(1f, IsPropertySaveable.No, description: "How likely is the AI to change the state when this effect executes? 1 = always, 0.5 = 50% chance, 0 = never.")] + public float Probability { get; private set; } + + [Serialize(0f, IsPropertySaveable.No, description: + "How much damage the character must receive for this AITrigger to become active? " + + "Checks the amount of damage the latest attack did to the character.")] + public float MinDamage { get; private set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger override other active AITriggers?")] + public bool AllowToOverride { get; private set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger be overridden by other AITriggers?")] + public bool AllowToBeOverridden { get; private set; } + + public bool IsTriggered { get; private set; } + + public float Timer { get; private set; } + + public bool IsActive { get; private set; } + + public bool IsPermanent { get; private set; } + + public void Launch() + { + IsTriggered = true; + IsActive = true; + IsPermanent = Duration <= 0; + if (!IsPermanent) + { + Timer = Duration; + } + } + + public void Reset() + { + IsTriggered = false; + IsActive = false; + Timer = 0; + } + + public void UpdateTimer(float deltaTime) + { + if (IsPermanent) { return; } + Timer -= deltaTime; + if (Timer < 0) + { + Timer = 0; + IsActive = false; + } + } + + public AITrigger(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + + + /// + /// What should this status effect be applied on? + /// private readonly TargetType targetTypes; /// - /// Index of the slot the target must be in when targeting a Contained item + /// Index of the slot the target must be in. Only valid when targeting a Contained item. /// public int TargetSlot = -1; - private readonly List requiredItems; + private readonly List requiredItems = new List(); - public readonly Identifier[] propertyNames; - public readonly object[] propertyEffects; + public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects; - private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; + private readonly PropertyConditional.LogicalOperatorType conditionalLogicalOperator = PropertyConditional.LogicalOperatorType.Or; private readonly List propertyConditionals; public bool HasConditions => propertyConditionals != null && propertyConditionals.Any(); + /// + /// If set to true, the effect will set the properties of the target to the given values, instead of incrementing them by the given value. + /// private readonly bool setValue; + /// + /// If set to true, the values will not be multiplied by the elapsed time. + /// In other words, the values are treated as an increase per frame, as opposed to an increase per second. + /// Useful for effects that are intended to just run for one frame (e.g. firing a gun, an explosion). + /// private readonly bool disableDeltaTime; + /// + /// Can be used in conditionals to check if a StatusEffect with a specific tag is currently running. Only relevant for effects with a non-zero duration. + /// private readonly HashSet tags; - private readonly float duration; + /// + /// How long _can_ the event run (in seconds). The difference to is that + /// lifetime doesn't force the effect to run for the given amount of time, only restricts how + /// long it can run in total. For example, you could have an effect that makes a projectile + /// emit particles for 1 second when it's active, and not do anything after that. + /// private readonly float lifeTime; private float lifeTimer; public Dictionary intervalTimers = new Dictionary(); + /// + /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). + /// + private readonly bool oneShot; + public static readonly List DurationList = new List(); /// - /// Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be done when the effect triggers, + /// or for the whole duration it executes / when the delay runs out and the effect executes? In other words, if false, the conditionals + /// are only checked once when the effect triggers, but after that it can keep running for the whole duration, or is + /// guaranteed to execute after the delay. /// public readonly bool CheckConditionalAlways; /// - /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied? + /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s) if the effect is already being applied? /// public readonly bool Stackable = true; @@ -367,6 +555,9 @@ namespace Barotrauma public readonly float Interval; #if CLIENT + /// + /// Should the sound(s) configured in the effect be played if the required items aren't found? + /// private readonly bool playSoundOnRequiredItemFailure = false; #endif @@ -377,40 +568,79 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - public readonly List Explosions; + public readonly List Explosions = new List(); - private readonly List spawnItems; + private readonly List spawnItems = new List(); + + /// + /// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them. + /// private readonly bool spawnItemRandomly; - private readonly List spawnCharacters; + private readonly List spawnCharacters = new List(); - public readonly List giveTalentInfos; + public readonly List giveTalentInfos = new List(); - private readonly List aiTriggers; + private readonly List aiTriggers = new List(); - private readonly List triggeredEvents; - private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(), - triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); + private readonly List triggeredEvents = new List(); + + /// + /// If the effect triggers a scripted event, the target of this effect is added as a target for the event using the specified tag. + /// For example, an item could have an effect that executes when used on some character, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventTargetTag; + + /// + /// If the effect triggers a scripted event, the entity executing this effect is added as a target for the event using the specified tag. + /// For example, a character could have an effect that executes when the character takes damage, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventEntityTag; + + /// + /// If the effect triggers a scripted event, the user of the StatusEffect (= the character who caused it to happen, e.g. a character who used an item) is added as a target for the event using the specified tag. + /// For example, a gun could have an effect that executes when a character uses it, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventUserTag; private Character user; public readonly float FireSize; + /// + /// Which types of limbs this effect can target? Only valid when targeting characters or limbs. + /// public readonly LimbType[] targetLimbs; + /// + /// The probability of severing a limb damaged by this status effect. Only valid when targeting characters or limbs. + /// public readonly float SeverLimbsProbability; public PhysicsBody sourceBody; + /// + /// If enabled, this effect can only execute inside a hull. + /// public readonly bool OnlyInside; + /// + /// If enabled, this effect can only execute outside hulls. + /// public readonly bool OnlyOutside; - // Currently only used for OnDamaged. TODO: is there a better, more generic way to do this? - public readonly bool OnlyPlayerTriggered; /// - /// Can the StatusEffect be applied when the item applying it is broken + /// If enabled, the effect only executes when the entity receives damage from a player character + /// (a character controlled by a human player). Only valid for characters, and effects of the type . + /// + public readonly bool OnlyWhenDamagedByPlayer; + + /// + /// Can the StatusEffect be applied when the item applying it is broken? /// public readonly bool AllowWhenBroken = false; + /// + /// Identifier(s), tag(s) or species name(s) of the entity the effect can target. Null if there's no identifiers. + /// public readonly ImmutableHashSet TargetIdentifiers; /// @@ -424,8 +654,13 @@ namespace Barotrauma { get; private set; - } + } = new List(); + /// + /// Should the affliction strength be directly proportional to the maximum vitality of the character? + /// In other words, when enabled, the strength of the affliction(s) caused by this effect is higher on higher-vitality characters. + /// Can be used to make characters take the same relative amount of damage regardless of their maximum vitality. + /// private readonly bool? multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters @@ -433,21 +668,33 @@ namespace Barotrauma get { return spawnCharacters; } } - public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction; + public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>(); - private readonly List talentTriggers; - private readonly List giveExperiences; - private readonly List giveSkills; + private readonly List talentTriggers = new List(); + private readonly List giveExperiences = new List(); + private readonly List giveSkills = new List(); - public float Duration => duration; + /// + /// How long the effect runs (in seconds). Note that if is true, + /// there can be multiple instances of the effect running at a time. + /// In other words, if the effect has a duration and executes every frame, you probably want + /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. + /// + public readonly float Duration; - //only applicable if targeting NearbyCharacters or NearbyItems + /// + /// How close to the entity executing the effect the targets must be. Only applicable if targeting NearbyCharacters or NearbyItems. + /// public float Range { get; private set; } + /// + /// An offset added to the position of the effect is executed at. Only relevant if the effect does something where position matters, + /// for example emitting particles or explosions, spawning something or playing sounds. + /// public Vector2 Offset { get; private set; } public string Tags @@ -467,6 +714,8 @@ namespace Barotrauma } } + public bool Disabled { get; private set; } + public static StatusEffect Load(ContentXElement element, string parentDebugName) { if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null) @@ -479,34 +728,21 @@ namespace Barotrauma protected StatusEffect(ContentXElement element, string parentDebugName) { - requiredItems = new List(); - spawnItems = new List(); - spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); - spawnCharacters = new List(); - giveTalentInfos = new List(); - aiTriggers = new List(); - Afflictions = new List(); - Explosions = new List(); - triggeredEvents = new List(); - ReduceAffliction = new List<(Identifier affliction, float amount)>(); - talentTriggers = new List(); - giveExperiences = new List(); - giveSkills = new List(); - var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); - if (multiplyAfflictionsElement != null) - { - multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); - } - tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); - OnlyPlayerTriggered = element.GetAttributeBool("onlyplayertriggered", false); + OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false)); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); - TargetSlot = element.GetAttributeInt("targetslot", -1); - Interval = element.GetAttributeFloat("interval", 0.0f); + Duration = element.GetAttributeFloat("duration", 0.0f); + disableDeltaTime = element.GetAttributeBool("disabledeltatime", false); + setValue = element.GetAttributeBool("setvalue", false); + Stackable = element.GetAttributeBool("stackable", true); + lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f); + CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false); + + TargetSlot = element.GetAttributeInt("targetslot", -1); Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -521,9 +757,7 @@ namespace Barotrauma if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } - IEnumerable attributes = element.Attributes(); - List propertyAttributes = new List(); - propertyConditionals = new List(); + SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f); string[] targetTypesStr = element.GetAttributeStringArray("target", null) ?? @@ -532,7 +766,7 @@ namespace Barotrauma { if (!Enum.TryParse(s, true, out TargetType targetType)) { - DebugConsole.ThrowError("Invalid target type \"" + s + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid target type \"{s}\" in StatusEffect ({parentDebugName})"); } else { @@ -540,37 +774,55 @@ namespace Barotrauma } } - foreach (XAttribute attribute in attributes) + var targetIdentifiers = element.GetAttributeIdentifierArray(Array.Empty(), "targetnames", "targets", "targetidentifiers", "targettags"); + if (targetIdentifiers.Any()) + { + TargetIdentifiers = targetIdentifiers.ToImmutableHashSet(); + } + + triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", Identifier.Empty); + triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", Identifier.Empty); + triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", Identifier.Empty); + + spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); + + var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); + if (multiplyAfflictionsElement != null) + { + multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); + } + +#if CLIENT + playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false); +#endif + + List propertyAttributes = new List(); + propertyConditionals = new List(); + foreach (XAttribute attribute in element.Attributes()) { switch (attribute.Name.ToString().ToLowerInvariant()) { case "type": if (!Enum.TryParse(attribute.Value, true, out type)) { - DebugConsole.ThrowError("Invalid action type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid action type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } break; case "targettype": case "target": - break; - case "disabledeltatime": - disableDeltaTime = attribute.GetAttributeBool(false); - break; - case "setvalue": - setValue = attribute.GetAttributeBool(false); - break; - case "severlimbs": - case "severlimbsprobability": - SeverLimbsProbability = MathHelper.Clamp(attribute.GetAttributeFloat(0.0f), 0.0f, 1.0f); - break; case "targetnames": case "targets": case "targetidentifiers": case "targettags": - TargetIdentifiers = attribute.Value.Split(',').ToIdentifiers().ToImmutableHashSet(); + case "severlimbs": + case "targetlimb": + case "delay": + case "interval": + //aliases for fields we're already reading above, and which shouldn't be interpreted as values we're trying to set break; case "allowedafflictions": case "requiredafflictions": + //backwards compatibility, should be defined as child elements instead string[] types = attribute.Value.Split(','); requiredAfflictions ??= new HashSet<(Identifier, float)>(); for (int i = 0; i < types.Length; i++) @@ -578,43 +830,15 @@ namespace Barotrauma requiredAfflictions.Add((types[i].Trim().ToIdentifier(), 0.0f)); } break; - case "duration": - duration = attribute.GetAttributeFloat(0.0f); - break; - case "stackable": - Stackable = attribute.GetAttributeBool(true); - break; - case "lifetime": - lifeTime = attribute.GetAttributeFloat(0); - lifeTimer = lifeTime; - break; - case "eventtargettag": - triggeredEventTargetTag = attribute.Value.ToIdentifier(); - break; - case "evententitytag": - triggeredEventEntityTag = attribute.Value.ToIdentifier(); - break; - case "checkconditionalalways": - CheckConditionalAlways = attribute.GetAttributeBool(false); - break; case "conditionalcomparison": case "comparison": - if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) + if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator)) { - DebugConsole.ThrowError("Invalid conditional comparison type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } break; -#if CLIENT - case "playsoundonrequireditemfailure": - playSoundOnRequiredItemFailure = attribute.GetAttributeBool(false); - break; -#endif case "sound": - DebugConsole.ThrowError("Error in StatusEffect " + element.Parent.Name.ToString() + - " - sounds should be defined as child elements of the StatusEffect, not as attributes."); - break; - case "delay": - case "interval": + DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes."); break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -622,32 +846,30 @@ namespace Barotrauma propertyAttributes.Add(attribute); } break; + case "tags": + if (Duration <= 0.0f || setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags + propertyAttributes.Add(attribute); + } + break; + case "oneshot": + oneShot = attribute.GetAttributeBool(false); + break; default: + if (FieldNames.Contains(attribute.Name.ToIdentifier())) { continue; } propertyAttributes.Add(attribute); break; } } - if (duration > 0.0f && !setValue) - { - //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: - //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. - propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); - } - - int count = propertyAttributes.Count; - - propertyNames = new Identifier[count]; - propertyEffects = new object[count]; - - int n = 0; + List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>(); foreach (XAttribute attribute in propertyAttributes) { - - propertyNames[n] = attribute.NameAsIdentifier(); - propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); - n++; + propertyEffects.Add((attribute.NameAsIdentifier(), XMLExtensions.GetAttributeObject(attribute))); } + PropertyEffects = propertyEffects.ToImmutableArray(); foreach (var subElement in element.Elements()) { @@ -704,13 +926,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - propertyConditionals.Add(new PropertyConditional(attribute)); - } - } + propertyConditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "affliction": AfflictionPrefab afflictionPrefab; @@ -817,13 +1033,11 @@ namespace Barotrauma public bool ReducesItemCondition() { - for (int i = 0; i < propertyNames.Length; i++) + foreach (var (propertyName, value) in PropertyEffects) { - if (propertyNames[i] != "condition") { continue; } - object propertyEffect = propertyEffects[i]; - if (propertyEffect.GetType() == typeof(float)) + if (propertyName == "condition" && value.GetType() == typeof(float)) { - return (float)propertyEffect < 0.0f || (setValue && (float)propertyEffect <= 0.0f); + return (float)value < 0.0f || (setValue && (float)value <= 0.0f); } } return false; @@ -831,13 +1045,11 @@ namespace Barotrauma public bool IncreasesItemCondition() { - for (int i = 0; i < propertyNames.Length; i++) + foreach (var (propertyName, value) in PropertyEffects) { - if (propertyNames[i] != "condition") { continue; } - object propertyEffect = propertyEffects[i]; - if (propertyEffect.GetType() == typeof(float)) + if (propertyName == "condition" && value.GetType() == typeof(float)) { - return (float)propertyEffect > 0.0f || (setValue && (float)propertyEffect > 0.0f); + return (float)value > 0.0f || (setValue && (float)value > 0.0f); } } return false; @@ -851,7 +1063,7 @@ namespace Barotrauma } else { - return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.TargetTagMatchesTagCondition(t))); } } @@ -899,7 +1111,8 @@ namespace Barotrauma if (HasTargetType(TargetType.NearbyItems)) { //optimization for powered components that can be easily fetched from Powered.PoweredList - if (TargetIdentifiers.Count == 1 && + if (TargetIdentifiers != null && + TargetIdentifiers.Count == 1 && (TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent"))) { foreach (Powered powered in Powered.PoweredList) @@ -946,97 +1159,56 @@ namespace Barotrauma { if (conditionals.Count == 0) { return true; } if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } - switch (conditionalComparison) + + bool shortCircuitValue = conditionalLogicalOperator switch { - case PropertyConditional.Comparison.Or: - for (int i = 0; i < conditionals.Count; i++) + PropertyConditional.LogicalOperatorType.Or => true, + PropertyConditional.LogicalOperatorType.And => false, + _ => throw new NotImplementedException() + }; + + for (int i = 0; i < conditionals.Count; i++) + { + var pc = conditionals[i]; + if (!pc.TargetContainer || targetingContainer) + { + if (AnyTargetMatches(targets, pc.TargetItemComponent, pc) == shortCircuitValue) { return shortCircuitValue; } + continue; + } + + var target = FindTargetItemOrComponent(targets); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + bool comparisonIsNeq = pc.ComparisonOperator == PropertyConditional.ComparisonOperatorType.NotEquals; + if (comparisonIsNeq == shortCircuitValue) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - return true; - } - continue; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (pc.Matches(container)) { return true; } - } - else - { - if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } - } - } - if (owner is Character character && pc.Matches(character)) { return true; } - } - else - { - if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } - } + return shortCircuitValue; } - return false; - case PropertyConditional.Comparison.And: - for (int i = 0; i < conditionals.Count; i++) + continue; + } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - continue; - } - return false; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (!pc.Matches(container)) { return false; } - } - else - { - if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } - } - } - if (owner is Character character && !pc.Matches(character)) { return false; } - } - else - { - if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } - } + //if we're checking for tags, just check the Item object, not the ItemComponents + if (pc.Matches(container) == shortCircuitValue) { return shortCircuitValue; } } - return true; - default: - throw new NotImplementedException(); + else + { + if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc) == shortCircuitValue) { return shortCircuitValue; } + } + } + if (owner is Character character && pc.Matches(character) == shortCircuitValue) { return shortCircuitValue; } } + return !shortCircuitValue; static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) { @@ -1148,17 +1320,18 @@ namespace Barotrauma public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { + if (Disabled) { return; } if (this.type != type || !HasRequiredItems(entity)) { return; } if (!IsValidTarget(target)) { return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); if (existingEffect != null) { - existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1172,6 +1345,7 @@ namespace Barotrauma protected readonly List currentTargets = new List(); public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (this.type != type) { return; } if (ShouldWaitForInterval(entity, deltaTime)) { return; } @@ -1196,13 +1370,13 @@ namespace Barotrauma return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); if (existingEffect != null) { - existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1271,6 +1445,7 @@ namespace Barotrauma protected void Apply(float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (lifeTime > 0) { lifeTimer -= deltaTime; @@ -1395,16 +1570,16 @@ namespace Barotrauma } } - if (duration > 0.0f) + if (Duration > 0.0f) { - DurationList.Add(new DurationListElement(this, entity, targets, duration, user)); + DurationList.Add(new DurationListElement(this, entity, targets, Duration, user)); } else { for (int i = 0; i < targets.Count; i++) { var target = targets[i]; - if (target == null) { continue; } + if (target?.SerializableProperties == null) { continue; } if (target is Entity targetEntity) { if (targetEntity.Removed) { continue; } @@ -1414,14 +1589,13 @@ namespace Barotrauma if (limb.Removed) { continue; } position = limb.WorldPosition + Offset; } - - for (int j = 0; j < propertyNames.Length; j++) + foreach (var (propertyName, value) in PropertyEffects) { - if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[j], out SerializableProperty property)) + if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } - ApplyToProperty(target, property, j, deltaTime); + ApplyToProperty(target, property, value, deltaTime); } } } @@ -1437,11 +1611,10 @@ namespace Barotrauma { var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method - if (duration > 0) { break; } + if (Duration > 0) { break; } if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { - if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } Affliction newAffliction = affliction; if (target is Character character) { @@ -1506,7 +1679,7 @@ namespace Barotrauma { targetCharacter.TryAdjustHealerSkill(user, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, healthChange, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, -healthChange, 0.0f); #endif } } @@ -1635,18 +1808,20 @@ namespace Barotrauma { if (!triggeredEventTargetTag.IsEmpty) { - List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); - - if (eventTargets.Count > 0) + IEnumerable eventTargets = targets.Where(t => t is Entity); + if (eventTargets.Any()) { - scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets); + scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets.Cast().ToList()); } } - if (!triggeredEventEntityTag.IsEmpty && entity != null) { scriptedEvent.Targets.Add(triggeredEventEntityTag, new List { entity }); } + if (!triggeredEventUserTag.IsEmpty && user != null) + { + scriptedEvent.Targets.Add(triggeredEventUserTag, new List { user }); + } } } } @@ -1745,7 +1920,10 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandomUnsynced()); + if (spawnItems.Count > 0) + { + SpawnItem(spawnItems.GetRandomUnsynced()); + } } else { @@ -1886,9 +2064,15 @@ namespace Barotrauma } else if (entity is Item item) { - var itemContainer = item.GetComponent(); - inventory = itemContainer?.Inventory; - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + foreach (ItemContainer itemContainer in item.GetComponents()) + { + if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + inventory = itemContainer?.Inventory; + break; + } + } + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) { return; } @@ -1971,6 +2155,10 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + if (oneShot) + { + Disabled = true; + } if (Interval > 0.0f && entity != null) { intervalTimers[entity] = Interval; @@ -1992,16 +2180,15 @@ namespace Barotrauma partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull currentHull, Vector2 worldPosition, bool playSound); - private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, int effectIndex, float deltaTime) + private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - object propertyEffect = propertyEffects[effectIndex]; - if (propertyEffect is int || propertyEffect is float) + if (value is int || value is float) { float propertyValueF = property.GetFloatValue(target); if (property.PropertyType == typeof(float)) { - float floatValue = propertyEffect is float single ? single : (int)propertyEffect; + float floatValue = value is float single ? single : (int)value; floatValue *= deltaTime; if (!setValue) { @@ -2012,7 +2199,7 @@ namespace Barotrauma } else if (property.PropertyType == typeof(int)) { - int intValue = (int)(propertyEffect is float single ? single * deltaTime : (int)propertyEffect * deltaTime); + int intValue = (int)(value is float single ? single * deltaTime : (int)value * deltaTime); if (!setValue) { intValue += (int)propertyValueF; @@ -2021,12 +2208,12 @@ namespace Barotrauma return; } } - else if (propertyEffect is bool propertyValueBool) + else if (value is bool propertyValueBool) { property.TrySetValue(target, propertyValueBool); return; } - property.TrySetValue(target, propertyEffect); + property.TrySetValue(target, value); } public static void UpdateAll(float deltaTime) @@ -2055,15 +2242,16 @@ namespace Barotrauma foreach (ISerializableEntity target in element.Targets) { - for (int n = 0; n < element.Parent.propertyNames.Length; n++) + if (target?.SerializableProperties != null) { - if (target == null || - target.SerializableProperties == null || - !target.SerializableProperties.TryGetValue(element.Parent.propertyNames[n], out SerializableProperty property)) + foreach (var (propertyName, value) in element.Parent.PropertyEffects) { - continue; + if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) + { + continue; + } + element.Parent.ApplyToProperty(target, property, value, CoroutineManager.DeltaTime); } - element.Parent.ApplyToProperty(target, property, n, CoroutineManager.DeltaTime); } foreach (Affliction affliction in element.Parent.Afflictions) @@ -2120,7 +2308,7 @@ namespace Barotrauma { targetCharacter.TryAdjustHealerSkill(element.User, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, healthChange, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, -healthChange, 0.0f); #endif } } @@ -2144,18 +2332,23 @@ namespace Barotrauma private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime) { - float multiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; - if (entity is Item sourceItem && sourceItem.HasTag("medical")) + float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; + if (entity is Item sourceItem) { - multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user is not null) + if (sourceItem.HasTag("medical")) { - multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + if (user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + } + } + else if (sourceItem.HasTag(AfflictionPrefab.PoisonType) && user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - - return multiplier * AfflictionMultiplier; + return afflictionMultiplier * AfflictionMultiplier; } private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) @@ -2165,19 +2358,17 @@ namespace Barotrauma { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } - if (user is not null) { if (affliction.Prefab.IsBuff) { - afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.BuffItemApplyingMultiplier); } - else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") + else if (affliction.Prefab.Identifier == "organdamage" && targetCharacter.CharacterHealth.GetActiveAfflictionTags().Any(t => t == "poisoned")) { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { return affliction.CreateMultiplied(afflictionMultiplier, affliction); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 0cd9799f9..08658ad68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Steam { "language", 5 } }; - public static bool IsInitialized { get; private set; } + public static bool IsInitialized => IsInitializedProjectSpecific; private static readonly List popularTags = new List(); public static IEnumerable PopularTags @@ -189,7 +189,6 @@ namespace Barotrauma.Steam if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); } - IsInitialized = false; } public static IEnumerable ParseWorkshopIds(string workshopIdData) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 78cd9451d..5b6f6f15f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -194,7 +194,7 @@ namespace Barotrauma.Steam { try { - System.IO.Directory.Delete(item.Directory, recursive: true); + System.IO.Directory.Delete(item.Directory ?? "", recursive: true); } catch { @@ -312,7 +312,7 @@ namespace Barotrauma.Steam public static bool IsItemDirectoryUpToDate(in Steamworks.Ugc.Item item) { - string itemDirectory = item.Directory; + string itemDirectory = item.Directory ?? ""; return Directory.Exists(itemDirectory) && File.GetLastWriteTime(itemDirectory).ToUniversalTime() >= item.LatestUpdateTime; } @@ -403,7 +403,7 @@ namespace Barotrauma.Steam var ids = items.Select(it => it.Id.Value).ToHashSet(); var toUninstall = ContentPackageManager.WorkshopPackages .Where(pkg - => !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId) + => !pkg.UgcId.TryUnwrap(out var workshopId) || !ids.Contains(workshopId.Value)) .ToArray(); if (toUninstall.Any()) @@ -432,9 +432,9 @@ namespace Barotrauma.Steam if (!(itemNullable is { } item)) { return; } await Task.Yield(); - string itemTitle = item.Title.Trim(); + string itemTitle = item.Title?.Trim() ?? ""; UInt64 itemId = item.Id; - string itemDirectory = item.Directory; + string itemDirectory = item.Directory ?? ""; DateTime updateTime = item.LatestUpdateTime; if (!CanBeInstalled(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 09ebf269f..6fae4f6cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -13,7 +13,7 @@ namespace Barotrauma { private const float UpdateInterval = 1.0f; - private static HashSet unlockedAchievements = new HashSet(); + private static readonly HashSet unlockedAchievements = new HashSet(); public static bool CheatsEnabled = false; @@ -219,10 +219,10 @@ namespace Barotrauma UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } - public static void OnCampaignMetadataSet(Identifier identifier, object value) + public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false) { if (identifier.IsEmpty || value is null) { return; } - UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier()); + UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients); } public static void OnItemRepaired(Item item, Character fixer) @@ -236,6 +236,15 @@ namespace Barotrauma UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier()); } + public static void OnAfflictionReceived(Affliction affliction, Character character) + { + if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; } +#if CLIENT + if (GameMain.Client != null) { return; } +#endif + UnlockAchievement(character, affliction.Prefab.AchievementOnReceived); + } + public static void OnAfflictionRemoved(Affliction affliction, Character character) { if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; } @@ -433,7 +442,7 @@ namespace Barotrauma var charactersInSub = Character.CharacterList.FindAll(c => !c.IsDead && c.TeamID != CharacterTeamType.FriendlyNPC && - !(c.AIController is EnemyAIController) && + c.AIController is not EnemyAIController && (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost))); if (charactersInSub.Count == 1) @@ -515,7 +524,10 @@ namespace Barotrauma public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) { if (CheatsEnabled) { return; } - + if (Screen.Selected is { IsEditor: true }) { return; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TestGameMode) { return; } +#endif #if SERVER if (unlockClients && GameMain.Server != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs index 06f36cc62..67b03d501 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs @@ -11,6 +11,7 @@ namespace Barotrauma left = l; right = r; } + // TODO: should this be && instead of ||? public override bool Loaded => left.Loaded || right.Loaded; public override void RetrieveValue() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 60a8364ca..4d7ca053c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -37,8 +37,9 @@ namespace Barotrauma public static float InterpolateRotation(float previous, float current) { + //use a somewhat high epsilon - very small differences aren't visible + if (MathUtils.NearlyEqual(previous, current, epsilon: 0.02f)) { return current; } float angleDiff = MathUtils.GetShortestAngle(previous, current); - return previous + angleDiff * (float)alpha; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index f7b33b9aa..9114c6f6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -43,15 +43,32 @@ namespace Barotrauma } } - public int GetBuyPrice(int level, Location? location = null) + public int GetBuyPrice(int level, Location? location = null, ImmutableHashSet? characterList = null) { - int maxLevel = Prefab.GetMaxLevelForCurrentSub(); + float price = BasePrice; - if (level > maxLevel) { maxLevel = level; } + int maxLevel = Prefab.MaxLevel; - int price = BasePrice; - price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); - return location?.GetAdjustedMechanicalCost(price) ?? price; + float lerpAmount = maxLevel is 0 + ? level // avoid division by 0 + : level / (float)maxLevel; + + float priceMultiplier = MathHelper.Lerp(IncreaseLow, IncreaseHigh, lerpAmount); + price += price * (priceMultiplier / 100f); + + price = location?.GetAdjustedMechanicalCost((int)price) ?? price; + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + if (characterList.Any()) + { + if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); + } + return (int)price; } } @@ -194,11 +211,10 @@ namespace Barotrauma _ => throw new ArgumentOutOfRangeException() }; - public bool AppliesTo(SubmarineInfo sub) + public bool AppliesTo(SubmarineClass subClass, int subTier) { if (type is MaxLevelModType.Invalid) { return false; } - int subTier = sub.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0); @@ -211,9 +227,9 @@ namespace Barotrauma return subTier == tier; } - if (tierOrClass.TryGet(out SubmarineClass subClass)) + if (tierOrClass.TryGet(out SubmarineClass targetClass)) { - return sub.SubmarineClass == subClass; + return subClass == targetClass; } return false; @@ -492,15 +508,19 @@ namespace Barotrauma { int level = MaxLevel; - foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) - { - if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } - } + int tier = info.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); - level += modifier; + tier += modifier; + } + + tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier); + + foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) + { + if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); } } return level; @@ -518,23 +538,25 @@ namespace Barotrauma if (character is null) { return false; } if (!ResourceCosts.Any()) { return true; } - List allItems = character.Inventory.FindAllItems(recursive: true); + var allItems = CargoManager.FindAllItemsOnPlayerAndSub(character); + return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem)); } + // ReSharper disable PossibleMultipleEnumeration public bool TryTakeResources(Character character, int currentLevel) { - IEnumerable costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); + var costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); if (!costs.Any()) { return true; } - List allItems = character.Inventory.FindAllItems(recursive: true); + var inventoryItems = CargoManager.FindAllItemsOnPlayerAndSub(character); HashSet itemsToRemove = new HashSet(); foreach (UpgradeResourceCost cost in costs) { int amountNeeded = cost.Amount; - foreach (Item item in allItems.Where(cost.MatchesItem)) + foreach (Item item in inventoryItems.Where(cost.MatchesItem)) { itemsToRemove.Add(item); amountNeeded--; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index c1225eb7b..a7dedde89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -25,7 +25,7 @@ namespace Barotrauma if (!Done) { Mre.WaitOne(); } } } - private static List enqueuedTasks; + private static readonly List enqueuedTasks; static CrossThread() { enqueuedTasks = new List(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 8b736ad74..76671aea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -155,7 +155,6 @@ namespace Barotrauma public static float CurveAngle(float from, float to, float step) { - from = WrapAngleTwoPi(from); to = WrapAngleTwoPi(to); @@ -189,13 +188,7 @@ namespace Barotrauma { return 0.0f; } - - while (angle < 0) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.TwoPi) - angle -= MathHelper.TwoPi; - - return angle; + return PositiveModulo(angle, MathHelper.TwoPi); } /// @@ -207,13 +200,9 @@ namespace Barotrauma { return 0.0f; } - // Ensure that -pi <= angle < pi for both "from" and "to" - while (angle < -MathHelper.Pi) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.Pi) - angle -= MathHelper.TwoPi; - - return angle; + float min = -MathHelper.Pi; + float diffFromMin = angle - min; + return diffFromMin - (MathF.Floor(diffFromMin / MathHelper.TwoPi) * MathHelper.TwoPi) + min; } public static float GetShortestAngle(float from, float to) @@ -342,13 +331,13 @@ namespace Barotrauma if (axisAligned1.Y < axisAligned2.Y) { - if (y < axisAligned1.Y) return false; - if (y > axisAligned2.Y) return false; + if (y < axisAligned1.Y) { return false; } + if (y > axisAligned2.Y) { return false; } } else { - if (y > axisAligned1.Y) return false; - if (y < axisAligned2.Y) return false; + if (y > axisAligned1.Y) { return false; } + if (y < axisAligned2.Y) { return false; } } intersection = new Vector2(axisAligned1.X, y); @@ -364,13 +353,13 @@ namespace Barotrauma if (axisAligned1.X < axisAligned2.X) { - if (x < axisAligned1.X) return false; - if (x > axisAligned2.X) return false; + if (x < axisAligned1.X) { return false; } + if (x > axisAligned2.X) { return false; } } else { - if (x > axisAligned1.X) return false; - if (x < axisAligned2.X) return false; + if (x > axisAligned1.X) { return false; } + if (x < axisAligned2.X) { return false; } } intersection = new Vector2(x, axisAligned1.Y); @@ -901,23 +890,30 @@ namespace Barotrauma // https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp public static bool NearlyEqual(float a, float b, float epsilon = 0.0001f) { - float diff = Math.Abs(a - b); if (a == b) { - // shortcut, handles infinities + //shortcut, handles infinities return true; } - else if (a == 0 || b == 0 || diff < float.Epsilon) + + if (a == 0 || b == 0) { - // a or b is zero or both are extremely close to it - // relative error is less meaningful here - return diff < epsilon; + //if a or b is zero, relative error is less meaningful + return Math.Abs(a - b) < epsilon; } - else + + float absA = Math.Abs(a); + float absB = Math.Abs(b); + float absAB = absA + absB; + if (absAB < epsilon) { - // use relative error - return diff / (Math.Abs(a) + Math.Abs(b)) < epsilon; + // a and b extremely close to zero, relative error is less meaningful + return true; } + + float diff = Math.Abs(a - b); + // use relative error + return diff / absAB < epsilon; } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index c4ce599c0..9bc971557 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -1,9 +1,7 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using Barotrauma.IO; +using System; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -12,52 +10,6 @@ namespace Barotrauma { public class Md5Hash { - public static class Cache - { - private const string cachePath = "Data/hashcache.txt"; - - private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries - = new List<(string Path, Md5Hash Hash, DateTime DateTime)>(); - - public static void Load() - { - if (!File.Exists(cachePath)) { return; } - var lines = File.ReadAllLines(cachePath); - if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version) - { - for (int i = 1; i < lines.Length; i++) - { - string[] split = lines[i].Split('|'); - string path = split[0].CleanUpPathCrossPlatform(); - Md5Hash hash = Md5Hash.StringAsHash(split[1]); - DateTime? dateTime = null; - if (long.TryParse(split[2], out long dateTimeUlong)) - { - dateTime = DateTime.FromBinary(dateTimeUlong); - } - - if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path)) - { - Entries.Add((path, hash, dateTime.Value)); - } - } - } - } - - public static void Add(string path, Md5Hash hash, DateTime dateTime) - { - path = path.CleanUpPathCrossPlatform(); - Remove(path); - Entries.Add((path, hash, dateTime)); - } - - public static void Remove(string path) - { - path = path.CleanUpPathCrossPlatform(); - Entries.RemoveAll(e => e.Path == path); - } - } - public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); private static string RemoveWhitespace(string s) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs deleted file mode 100644 index db6e813b9..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Barotrauma -{ - public sealed class None : Option - { - private None() { } - - public static Option Create() => new None(); - - public override Option Fallback(Option fallback) => fallback; - public override T Fallback(T fallback) => fallback; - - public override bool ValueEquals(T value) => false; - - public override string ToString() - => $"None<{typeof(T).Name}>"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 9aff08c3f..112281e50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,60 +1,83 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { - /// - /// Implementation of Option type. - /// - /// - /// Credit Jlobblet - /// - public abstract class Option + public readonly struct Option where T : notnull { - public static Option Some(T value) => Some.Create(value); - public static Option None() => None.Create(); - public bool IsNone() => this is None; - public bool IsSome() => this is Some; + private readonly bool hasValue; + private readonly T? value; - public bool TryUnwrap(out T outValue) => TryUnwrap(out outValue); - - public bool TryUnwrap(out T1 outValue) where T1 : T + private Option(bool hasValue, T? value) { - switch (this) - { - case Some { Value: T1 value }: - outValue = value; - return true; - default: - outValue = default!; - return false; - } + this.hasValue = hasValue; + this.value = value; } - public Option Select(Func selector) => - this switch + public bool IsSome() => hasValue; + public bool IsNone() => !IsSome(); + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T1? outValue) where T1 : T + { + bool hasValueOfGivenType = false; + outValue = default; + + if (hasValue && value is T1 t1) { - Some { Value: var value } => Option.Some(selector.Invoke(value)), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException() + hasValueOfGivenType = true; + outValue = t1; + } + + return hasValueOfGivenType; + } + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue) + => TryUnwrap(out outValue); + + public Option Select(Func selector) where TType : notnull + => TryUnwrap(out T? selfValue) ? Option.Some(selector(selfValue)) : Option.None; + + public Option Bind(Func> binder) where TType : notnull + => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + + public T Fallback(T fallback) + => TryUnwrap(out var v) ? v : fallback; + + public Option Fallback(Option fallback) + => IsSome() ? this : fallback; + + public static Option Some(T value) + => typeof(T) switch + { + var t when t == typeof(bool) + => throw new Exception("Option type rejects booleans"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Option<>) + => throw new Exception("Option type rejects nested Option"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Nullable<>) + => throw new Exception("Option type rejects Nullable"), + _ + => new Option(hasValue: true, value: value ?? throw new Exception("Option type rejects null")) }; - public abstract Option Fallback(Option fallback); - public abstract T Fallback(T fallback); - - public abstract bool ValueEquals(T value); - public override bool Equals(object? obj) => obj switch { - Some { Value: var value } => this is Some { Value: { } selfValue } && selfValue.Equals(value), - None _ => IsNone(), - T value => this is Some { Value: { } selfValue } && selfValue.Equals(value), - _ => false + Option otherOption when otherOption.IsNone() + => IsNone(), + Option otherOption when otherOption.TryUnwrap(out var otherValue) + => ValueEquals(otherValue), + T otherValue + => ValueEquals(otherValue), + _ + => false }; + public bool ValueEquals(T otherValue) + => TryUnwrap(out T? selfValue) && selfValue.Equals(otherValue); + public override int GetHashCode() - => this is Some { Value: { } value } ? value.GetHashCode() : 0; + => TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0; public static bool operator ==(Option a, Option b) => a.Equals(b); @@ -62,22 +85,28 @@ namespace Barotrauma public static bool operator !=(Option a, Option b) => !(a == b); - public abstract override string ToString(); - - public static implicit operator Option(Option.UnspecifiedNone _) + public static Option None() + => default; + + public static implicit operator Option(in Option.UnspecifiedNone _) => None(); + + public override string ToString() + => TryUnwrap(out var selfValue) + ? $"Some<{typeof(T).Name}>({selfValue})" + : $"None<{typeof(T).Name}>"; } public static class Option { - public sealed class UnspecifiedNone + public static Option Some(T value) where T : notnull + => Option.Some(value); + + public static UnspecifiedNone None + => default; + + public readonly ref struct UnspecifiedNone { - private UnspecifiedNone() { } - internal static readonly UnspecifiedNone Instance = new(); } - - public static UnspecifiedNone None => UnspecifiedNone.Instance; - - public static Option Some(T value) => Option.Some(value); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs deleted file mode 100644 index 5fd1dc3b0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Barotrauma -{ - public sealed class Some : Option - { - public readonly T Value; - - private Some(T value) - { - if (value is null) { throw new ArgumentNullException(nameof(value), "Some cannot contain null"); } - Value = value; - } - - public static Option Create(T value) => new Some(value); - - public override Option Fallback(Option fallback) => this; - public override T Fallback(T fallback) => Value; - - public override bool ValueEquals(T value) => Value.Equals(value); - - public override string ToString() - => $"Some<{typeof(T).Name}>({Value})"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 3c6b11b64..9429dcf93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -23,7 +23,7 @@ namespace Barotrauma return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T))); } - public static Option ParseDerived(TInput input) where TInput : notnull + public static Option ParseDerived(TInput input) where TInput : notnull where TBase : notnull { static Option none() => Option.None(); @@ -54,10 +54,10 @@ namespace Barotrauma f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) - as Option ?? none(); + as Option? ?? none(); } - return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); + return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()); } public static string NameWithGenerics(this Type t) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs index 9f65e0ef8..49c50dc1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs @@ -141,9 +141,8 @@ namespace Barotrauma public SerializableDateTime ToLocal() => new SerializableDateTime( - DateTime.SpecifyKind( - value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, - DateTimeKind.Local)); + new DateTime(ticks: value.Ticks) - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, + SerializableTimeZone.LocalTimeZone); public long Ticks => value.Ticks; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 08254fa0d..9cbc56614 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -394,7 +394,6 @@ namespace Barotrauma return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync)); } - public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) { List objectList = objects.ToList(); @@ -409,7 +408,7 @@ namespace Barotrauma public static T SelectWeightedRandom(IList objects, IList weights, Random random) { - if (objects.Count == 0) return default(T); + if (objects.Count == 0) { return default(T); } if (objects.Count != weights.Count) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 8f63361d2..be94e2df8 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,418 @@ +--------------------------------------------------------------------------------------------------------- +v1.1.4.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +- Fixed Tormsdale mission not completing unless you bring the item to the sub. Now retrieving the item counts as "bringing it to the sub" in friendly outposts. + +WIP countermeasures against multiplayer exploits (feedback appreciated!): +- Fixed an exploit that allowed you to equip 2-handed weapons in only one hand. +- Added protection against deliberately lagging the server. + - Added option to enable "DoS Protection" in the server settings under "Anti-Griefing". + - Enabled by default. + - When enabled, the server will automatically kick players who are causing the server to perform poorly. + - Added a new "Max Packet Auto-Kick" in the server settings under "Anti-Griefing". + - Enabled and set to 2400 by default. + - Can be disabled by setting the limit to 1200 or below. + - When enabled, the server will automatically kick players who are sending a certain amount of network packets in a minute. +- Added "Spam Immunity" server permission. + - Gives immunity to getting kicked from DoS protection, chat spam and from sending too many packets. +- Added a rate limit to console commands in multiplayer. +- Added a rate limit to creating new characters in multiplayer. + +Unstable only: +- Fixed frequent "collection was modified" errors when loading/removing/copying structures. +- Fixed light textures being misaligned, causing many areas to look unintentionally bright. +- Fixed artifact transport case not having a background behind the fans, allowing you to see through the edges of the fans. + Added the fans to the inventory icon. +- Fixed character sinking downwards in "steps" when close to the surface of water and when the water level is going down. + +--------------------------------------------------------------------------------------------------------- +v1.1.3.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed broken "dialoglowrepcampaigninteraction" NPC line. +- Fixed messed up water effect and lighting. + +--------------------------------------------------------------------------------------------------------- +v1.1.2.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed most StatusEffects doing nothing. + +--------------------------------------------------------------------------------------------------------- +v1.1.1.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed RelatedItem not assigning the Identifiers or ExcludedIdentifiers properties, causing various kinds of crashes in various places. +- Fixed a race condition that sometimes caused "collection was modified" exceptions when rendering lights. + +--------------------------------------------------------------------------------------------------------- +v1.1.0.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Line of sight rework: +- Improved the LOS effect to get rid of weird, jagged geometry in spots with intersecting walls. +- Improved the wall damage effect to make leaks easier to see. +- Added a LOS setting that only obstructs visibility through the sub's exterior walls. + +Changes and additions: +- Oxygen generator sprite and animation fixes. +- Optimized/simplified exosuit and FB3000 status effects. +- Optimized flashlights (and other "spotlight" type of light sources): previously they'd calculate shadows all around them, even though the light is only visible in front of the light source. +- Partially multithreaded lighting: FindRaycastHits (the heaviest individual part of the lighting algorithm) is now handled in a separate thread. +- Spawn abyss and combat suits in enemy subs and wrecks instead of normal ones in later biomes. +- Outpost NPCs don't allow players to grab them for longer than 10 seconds to prevent being able to drag them around the outpost. +- Unconscious players can't end the round. +- Submarine upgrades that require materials can be purchased with materials in the sub, instead of having to carry the materials on you. +- Added biome-specific outpost level generation parameters (i.e. outpost levels look different in different biomes). +- Minor visual improvements to Hydrothermal Wastes and The Great Sea. +- Faction reputation is reset after finishing the campaign. +- Adjusted FB3000 fabrication recipe (the previous required so many materials they don't fit in the fabricator's input slots). +- Fixed misplaced sonar beacon around the end of the campaign, causing the sonar to display an "emergency signal" outside the level. +- Added an event on the round after rescuing Subra (a sort of escort mission, it's weird if he just disappears after the rescue mission). +- Changed sonar flora sprite depths to prevent them from being obscured by the edge chunk objects. +- Restrict the maximum reputation loss from damaging NPCs and walls to 10 per round. Could use some further adjustment (feedback welcome!), but now a trigger-happy player can't cause too much permanent damage, but 10 is still a sizable penalty. +- Halved the reputation losses from damaging NPCs and walls. The previous values seemed too punishing now that the reputation loss has much more long-lasting consequences. +- Adjusted music intensity ranges to make the actual "music" (as opposed to the intensity tracks) play more frequently. +- Artifact transport cases require batteries to nullify the effects of the artifacts. The batteries last a little over 8 minutes. Also added some animations and lights to the case when powered. +- Allow stealing items in trash cans. +- Made sulphurite shards explode when thrown. +- Outpost NPCs who offer services don't get turned hostile by low reputation (but have some special dialog lines when you interact with them). + +Fixes: +- Fixed crashing when you exit Steam while the Workshop menu is open. +- Fixed crashing with an error message about OutpostGenerationParams.CanHaveCampaignInteraction when some outpost NPCs defined in outpost generation params can't be found. +- Fixed outpost NPCs never attacking you if your reputation is not low. +- Fixed characters holding and eating bananas weirdly. :) +- Fixed inability to hire Jacov Subra if you miss/ignore the event the first time you get it. +- Fixed all lights always going through walls in outposts. +- Fixed water detector in Dugong's oxygen generator room not being connected to the flood alarm circuit. +- Fixes to UI layout on ultrawide resolutions. +- Fixed ragdolls sometimes getting stuck on corners near platforms. +- Fixed characters not falling through holes in the floor below them until they move. +- Fixed inaccurate "Steady Tune" description. +- Fixed autoinjectors doing nothing. +- Fixed characters sometimes dying from barotrauma despite the pressure icon not being visible when in a partially pressurized hull. +- Outpost generator only takes walls with a collider into account when determining the bounds of the modules. Fixes husk modules being placed unnecessarily far from the outpost due to the decorative structures outside the door, leaving a very short hallway between the modules. +- Fixed character interaction texts (like "[H] Heal") not changing when you change the language. +- Fixed arc emitter briefly stunning the user client-side when fired. +- Fixed CustomInterface not showing the labels correctly in the sub editor (displaying the text tag instead of the actual text). +- Fixed operate order category not getting highlighted for the turret objective in the campaign tutorial. +- Fixed characters sometimes becoming immobile for a moment (as if they were briefly stunned) when the surface of the water raises above their chest. +- Fixed outpost NPCs who were supposed to stay in the room they spawn in (e.g. NPCs offering outpost services) occasionally wandering out of the room. +- Fixed pirates / bot crews being unable to swap between weapons and improved how they decide which turret to use (allowing them to swap to a turret with better visibility to the target). +- Fixed monsters sometimes spawning right next to pirate subs. +- Fixed "Gene Harvester" talent spawning genetic materials on pets. +- Fixed rebinding the use key not working in the sub editor. +- Fixed Ctrl+A not selecting connected wires in the sub editor. +- Fixed cancelling fabrication always enabling the amount slider, even if you can only fabricate one. +- Made beds secondary items. Fixes being able to climb ladders while in bed. +- Fabricator input slot tooltips don't show duplicate item names when the item can be crafted from multiple different items with the same name (e.g. petraptor egg can be crafted from 3 different egg items, all called "mudraptor egg"). +- Fixed attacking others with the husk appendage not healing the user. +- Fixed headsets being drawn in front of helmets when you equip the helmet 1st and then the headset. +- Dying due to a disconnect doesn't trigger talents (like "Revenge Squad") or get recorded as a kill. +- Fixed pet name tag getting stuck mid-air when equipped. +- Fixed next round's missions not being displayed in the round summary when leaving a location that has missions (e.g. outpost with a jailbreak mission). +- Fixed turrets being able to launch projectiles inside enemy subs if you poke the turret through the sub's hull. +- Fixed an issue that sometimes caused some levels to always display as unvisited and unlocked (more specifically, the level that happened to generate 1st during the campaign map generation). +- Fixed treatment suggestion for husk infection being shown when wearing zealot robes. +- Fixed all crates and ammo boxes having slightly too large colliders, making them float above ground. +- Fixed misaligned pulse laser sprites. +- Fixed fabricators not being linked to the cabinet next to them in Azimuth, Berilia, Kastrull, Orca 2, Typhon, Typhon 2 and Winterhalter. +- Fixed inability to fabricate high-quality nuclear depth charges. +- Fixed exosuits getting autofilled with batteries despite being powered by fuel rods now. +- Fixed exosuits' lights not turning off when the wearer dies. +- Fixed exosuit not muffling sounds when worn. +- Fixed cultist hood overlapping with the exosuit. +- Fixed escort missions sometimes unlocking in a level leading to an abandoned outpost even if there's an inhabited one available. +- Fixed campaign's end boss moving away from you if you attack it with melee weapons. +- Fixed projectile spread not working properly, causing the projectiles to be launched at the same angle too often. +- Fixed some of the fonts not working properly in Japanese, displaying roughly similar Chinese symbols instead of the correct Japanese symbols. +- Fixed pirate captain hats peeking through PUCS's helmet. +- Fixed escorted characters being hostile if they belong to a faction you have a low reputation with. +- Reduce the minimum mass required for a character to be visible with thermal goggles, always show at least the main limb regardless of the mass. Fixes thresher hatchlings being invisible to the goggles. +- Fixed medical clinic sometimes displaying healths as 99% despite the character having seemingly no afflictions. +- Fixed colony docking modules spawning with a bit of water in them. +- Fixed artifacts being slightly off-center and at an incorrect sprite depth in artifact holders. +- Fixed minerals sometimes spawning in normal caves in abyss mining missions. Happened if no abyss islands with caves happened to generate - now we always generate a cave in at least one of the islands. +- Fixed lights on the items the character is wearing being visible when inside a clown crate. +- Fixed mudraptor eggs (or other items set to be damaged by repair tools) not being damaged by flamer. +- Fixed wikiimage_sub not sorting the entities the same way as the sub editor and game screen, causing e.g. doors to render in front of walls. +- Fixed only main subs's sonar working properly in the end levels. +- Fixed "enablecheats" resetting when you save and reload a campaign save, meaning you could enable cheats, use them to e.g. spawn some weapons, save and reload, and then continue unlocking achievements in that same save. +- Fixed achievements being unlockable in editors. +- Fixed docking ports sometimes becoming impassable when the sub undocks and docks when there's an obstacle right outside it. Happened, for example, with certain kinds of elevators built using linked submarines. +- Fixed orders persisting even if the target no longer exists after a sub switch. +- Fixed reputation reward text sometimes overflowing in the round summary (e.g. when the mission modifies both the husk cult and clown rep). +- Fixed all friendly characters using the "hostage" dialog and all hostile characters the "bandit" dialog in abandoned outposts. +- Fixed "fight intruders" order causing bots to attack enemies in abandoned outposts again. +- A safeguard against getting pinned under a flooded pirate sub: if your sub is under an enemy sub with no living enemies inside it, and heading upwards, the submarine on top will gradually move up. +- Fixed subs sometimes getting stuck if they try to squeeze through a too small passage in the level. +- Fixed monsters sometimes spawning inside the sub during beacon missions. +- Fixed monsters spawned by nest missions still sometimes spawning inside a wall. +- Fixed hanging end of the wire sometimes being "at the wrong end" of the wire after copying entities or saving/reloading. +- Fixed artifacts never spawning randomly in ruins: we tried to spawn them in containers with the tag "ruintreasure", but those were all small chests which couldn't hold artifacts. I made artifacts containable in any alien chest now, and also have a 5% chance to spawning an artifact in a large chest. +- Fixed water moving erratically in rooms with lots of small, connected hulls. +- Fixed Medical Expertise not increasing bandage effectiveness. +- Wearing a clown suit without a mask gives you the "clown" status tag (making you play circus music on instruments and giving you immunity to to banana peels). + +Multiplayer: +- Changes to make starting a round more robust: fixes various equality check errors ("submarine/mission doesn't match") if starting a multiplayer round takes a long time. +- Fixed clients not seeing wall damage in outposts if the server has made outpost walls damageable, and the client doesn't have permissions to manage server settings. +- Fixed dedicated servers' content package info getting truncated to 255 bytes, causing the content package list to just display "unknown" if the server has lots of mods enabled. +- Fixed sonar beacon tickbox flickering on and off when interacted with in multiplayer. +- Fixes to oddities/inconsistencies when trying to heal someone while climbing ladders: dropping off ladders when opening your own health interface in MP, and "heal [H]" hint being visible when focusing on other characters while climbing ladders and the health interface opening for one frame if you attempt to heal. +- Fixed console errors when a client disconnects while a vote is running, and purchases being free if the vote goes through. +- Fixed "none" permission preset not working as it should when the language is set to something else than English (clients would not get assigned the "none" preset by default). +- Fixed 0% condition items in the character's inventory executing the OnBroken effects at the start of a round (i.e. empty stun and fixfoam grenades detonating in your inventory). +- Fixed pre-unlocked talents not being visible client-side until the next round when hiring one of the special faction NPCs. +- Fixed minerals spawning with their rotation set to 0 in multiplayer in mineral missions. +- Fixed ragdolls sometimes getting stuck on the wrong side of a door client-side, and not getting corrected until the client moves in the opposite direction or opens the door. +- Don't force campaign rounds to stop when the only client on the server is using freecam. +- Fixed spectators not hearing others before their character despawns when dead. +- Fixed light sprites being slightly too small on wrecked items. +- Fixed characters getting removed at the end of the round if they've died and then been revived with the "revive" console command. +- Mission unlock notifications aren't shown to people in the server lobby when a round is running. +- Fixed local voice chat icon switching to radio icon (from yellow to gray) at the end of conversations when you release the push-to-talk key. + +Modding: +- Fixed ability to "relaunch" a projectile that has already been launched or that's stuck to some target using status effects. +- Fixed LevelTrigger's OtherTrigger type not doing anything. +- Improvements to LevelObject culling to make objects less likely to disappear when they're in view and there's a too large number of objects visible. +- Fixed NearbyItems effects causing a crash if TargetIdentifiers haven't been set. +- Fixed LevelTrigger statuseffects not doing anything when triggered by a submarine. +- Fixed submarine spawning docked to a random docking port of a custom outpost module with multiple ports, even if one of them is marked as a main docking port. + +--------------------------------------------------------------------------------------------------------- +v1.0.8.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed loading screens sometimes getting stuck when playing in Chinese, Japanese or Korean. +- Fixed certain mods that override outpost generation parameters causing crashes due to missing outpost NPC prefabs. +- Fixed outpost NPCs never attacking you (just aiming their guns at you) if you attack them, but your reputation is not low enough to turn the outpost hostile. +- Fixed broken dialog line in the waytoascension1 event. +- Fixed healing your crewmates causing your karma to decrease. + +--------------------------------------------------------------------------------------------------------- +v1.0.7.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed mechanic tutorial getting stuck at the point where you need to weld a leak. +- Fixed treatment suggestions not showing up in the naloxone part of the medic tutorial. +- Fixed patient spawning dead in the CPR part of the medic tutorial. +- Fixed bots being unable to move through Herja's airlock due to an unlinked waypoint. +- Fixed tutorial Dugong spawning with empty ammo boxes. + +--------------------------------------------------------------------------------------------------------- +v1.0.6.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed some new Steam achievements not unlocking. + +--------------------------------------------------------------------------------------------------------- +v1.0.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixes to Japanese translations. +- Implemented support for some upcoming Steam achievements. +- Improved backwards compatibility: fixed outpost managers no longer spawning in mods that override outpost generation parameters due to the generic non-faction-specific outpost manager prefab being removed. + +--------------------------------------------------------------------------------------------------------- +v1.0.4.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed outpost NPCs getting randomized every time you re-enter an outpost. +- Fixed inability to gain more than 15 talent points in the multiplayer campaign. + +--------------------------------------------------------------------------------------------------------- +v1.0.3.0 +--------------------------------------------------------------------------------------------------------- + +- Adjusted plant spawn rates in caves. +- Made lead more common in stores. +- Fixed mission listing shown on the top of the screen spoiling enemy faction ambushes. +- Fixed enemy faction ambushes being possible everywhere on the map (they should only happen within 3 steps of outposts belonging to an enemy faction). +- Fixes bots sometimes running towards doors that are closed by something else (another person or an automatic logic). +- Fixes bots ordered to wait outside of the submarine not being able to switch their oxygen tanks. +- Added a couple of endgame-foreshadowing lore bits. + +--------------------------------------------------------------------------------------------------------- +v1.0.2.0 +--------------------------------------------------------------------------------------------------------- + +- Exosuits are powered by fuel rods instead of batteries. +- Fixed affliction probabilities being evaluated twice, meaning that e.g. 50% probability of getting some affliction from an attack was actually a 25% probability. +- Fixed item highlights from the previous round remaining visible the next round. +- Fixed swapping items in a container sometimes causing too many items to be visible in it. +- Fix the vitality modifiers on husk not working properly, because health indices on the limbs were not defined. Effectively husks always took 2x damage. +- Fixed characters still spawning inside outposts that have turned hostile due to low reputation. +- Fixed all special faction hire events getting stuck if you say you need to "think about it", return, and say you still need to think about it. +- Fixed missing "place in ceiling" text in beacon station save dialog. +- Fixed basic depth charges being cheaper than intended (only 30 mk). +- Fixed inability to make lights blink at a high frequency by rapidly turning them on and off with e.g. oscillators. +- Fixed ranged weapons emitting particles in the wrong direction. There haven't been any changes to this code in years, so it must've been an issue for a long time, I guess we just never noticed because no gun before the scrap cannon emitted particles with a noticeable velocity? +- Fixed a pathfinding issue that often made bots swim against cave walls. +- Fixed inability to join servers with a submarine switch/purchase vote running. +- Fixed votes passing if the client who initiated them disconnects before anyone else votes. +- Fixed follow orders not being persistent between singleplayer rounds. + +--------------------------------------------------------------------------------------------------------- +v1.0.1.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed Mailman talent giving you the 150 mk bonus every time you open the campaign map or mission menu and there's a cargo mission visible. +- Fixed missions available from the destination location to some other location being listed as "outpost missions" in the campaign map's mission selection. +- Fixed outposts that faction missions take place in being allowed to turn into abandoned outposts when next to hunting grounds, making the missions impossible to complete. +- Fixed hidden items (e.g. Separatist deco that's disabled in Coalition outposts) sometimes getting chosen as targets for scripted events, resulting in non-interactable, glowing "ghost items". +- Fixed mysterious floating status monitor in AdminModule_02_Colony. +- Miscellaneous optimizations. +- Fixed Separatist jailbreak mission causing a crash. +- Fixed ranged weapons (most noticeably, scrap cannon) emitting particles in the wrong direction. +- Fixed acid burns not having a cause of death text. +- Fixed "skedaddle" not giving a 10% movement boost like the description says. +- Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. +- Fixed red glow around the light switch's green button. +- Fixed banana being held weirdly. +- Fixed inability to hold a captain's pipe or cigar in your left hand. +- Fixed ready checks not working. + +--------------------------------------------------------------------------------------------------------- +v1.0.0.0 +--------------------------------------------------------------------------------------------------------- + +Faction overhaul: +- Outposts are controlled by the Europa Coalition or Jovian Separatists, and some of them include a module belonging to the Church of Husk or Children of the Honkmother. +- Got rid of location-specific reputation. Now all the events/missions give faction reputation instead (excluding missions that aren't related to or given by a faction, e.g. abandoned outpost missions). +- Lots of new outpost events, and a longer "event chain" for the secondary factions. +- Lots of new faction-specific missions: some variants of existing missions, some new. +- Faction-specific hires: "generic" high-level characters with more experience points and better gear than normal hireable NPCs. Available for hiring when Coalition or Separatist reputation is high enough. +- Special, named characters who can be hired via scripted events after reaching a high enough reputation. +- Faction-specific vendors (separatists, husks, clowns) who sell special items (many of which are completely new) if your reputation is high enough. +- If your Coalition/Separatist reputation is low enough, you may get attacked by their vessel during missions. +- There's now always two paths from biome to another, one controlled by the Coalition and one by the Separatists. +- Improvements to the campaign map. +- Added a 3rd talent tree, "Politician", for the Captain. Focused around faction relations and reputation. + +Endgame: +- Completely remade the ending of the campaign. Now you'll get to see what's beyond the Eye of Europa and perhaps uncover the cause for the increasing levels of radiation. +- New types of enemies/bosses. +- Some new events to foreshadow the ending during the course of the campaign. + +Misc changes: +- New loading screen / location portraits. +- Two new music tracks. +- Items' skill requirements are shown in their tooltips (the same way as damage resistances). +- Tweaks to poisons. +- Adjusted Europan Handshake to work better with the overhauled morbusine poisoning. +- Acid Grenades and 40mm Acid Grenades are now properly affected by talents +- Acid Grenades and 40mm Acid Grenades deal more damage and slow enemies down, making them more viable against fast monsters. +- Made regular 40mm grenades penetrate armor more efficiently. +- Made Diving Suits resist Acid Burns a bit more. +- Europa Brew's Acid Vulnerability is now double as effective (200% damage taken instead of 100%). +- Adjustment to throwable items (shorter throw distance and reduced speed in water). +- Made flares float in place to make them more useful. +- Made high-quality stun guns more effective (stunning the target faster). +- The health scanner always shows poisons and paralysis on monsters to make it easier to determine whether the poisoning is progressing or wearing off. +- A pass on sound ranges: the ranges should now be more consistent and sensible. +- Made moloch shell fragment and riot shield medium items instead of small to fix them going inside e.g. toolbelts. +- Made husk eggs consumable. +- Made it more difficult to repeatedly enter an abandoned outpost and re-loot the bandits: now the bandits immediately attack you if you re-enter the outpost. +- Monsters you haven't encountered yet are now hidden by default in the character editor. Can be enabled using the command "showmonsters" and re-hidden using "hidemonsters". The value is saved in creaturemetrics.xml. Doesn't affect custom creatures. +- Fixed character crush depths behaving inconsistently (varying between levels, e.g. sometimes crushing the character at the depth of 2000 meters, sometimes 3000). +- Improvements to submarine crush depth effects: previously the breaches were easy to deal with because pressure did small amounts of damage to all walls, now it instead does heavier damage to some walls (and the amount of damage and walls to damage increases with depth). +- Added a round light component variant. +- Increased the hard-coded max mission count back from 3 to 10. It'd be preferable to not change the value above 3 in the vanilla game, but since campaign settings are not moddable, we shouldn't be too strict about it (because it can be useful for a mod that this value can be adjusted). +- Miscellaneous optimizations. +- New slot indicator icons (= the icons that show what can go inside some items, like tanks/ammo). +- Made outpost hull repair service cheaper. +- Doors can now be damaged by melee weapons and ranged (handheld) weapons. (They were already destructible by submarine mounted weapons and explosives) +- Adjusted and rebalanced item damage for most items, to take into account doors being destructible. +- Reduced time needed for a crowbar to open doors, 7.5s for regular doors, 6s for wrecked doors (down from 10 s). +- Boosted Plasma Cutter damage against doors and items (walls not touched). +- Made galena more common in order to make lead easier to get. +- Added Auto Operate option for all turrets. Can be enabled in the submarine editor. Not currently used on vanilla submarines. Auto operated turrets don't require a person to operate them, but they still require power and ammunition (-> someone needs to reload them). + +Multiplayer: +- Added a language filter to the server browser. +- Fixed reports given by dragging and dropping them on the status monitor always targeting the room the character is inside. +- Improvements to medical clinic syncing (should fix some of the afflictions a character has sometimes not being visible on the list). +- Fixed crashing if you close a server when mod downloads are disabled. +- Improved projectile syncing: spread now behaves the same client-side as it does server-side (as opposed to being completely random). +- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen. +- Fixed server randomizing the game mode at the end of the round when playing a campaign with the game mode selection set to Random. + +AI: +- Fixed bots considering certain multi-hull rooms flooded when they are not. +- Fixed bots deciding prematurely that they can't fix an item when it's deteriorating (e.g. when it's submerged). +- Fixed bots removing battery cells from exosuits when ordered to charge batteries. +- Fixed bots sometimes getting stuck in automatic doors and/or double doors, because they didn't wait for the door to open entirely before pressing the button again. +- Improved bot ‘extinguish fires’ behavior. Fixes bots sometimes not being able to extinguish larger fires, because they stopped too far and didn't keep advancing towards the target. +- Fixed bots claiming that they can't return back to the sub and then following the order anyway. +- Improved the ‘find safety’ calculations so that the bots give more preference to the distance of the room. +- Fixed some remaining issues and edge cases in the logic over when the bot needs diving gear and when it can be taken off. +- Fixed captains (and some NPCs) idling in the airlock if they equip a diving suit. +- Bot can now target items (like projectiles) with turrets and have different targeting priorities on different monsters. +- Fixed bots being allowed to reach items that are too far to be interacted with. + +Fixes: +- Fixes and improvements to translations (Japanese and Chinese in particular). +- Fixed light components with a range of 0 and a hidden sprite being invisible against dark backgrounds. +- Various fixes to Typhon 1: most notably, adjusting the hulls to prevent some rooms from being impossible to drain fully. +- Fixed "kill" command not killing characters under the influence of "Miracle Worker". +- Fixed some lights becoming invisible when their range is set to 0 and they're against a dark background. +- Fixed lights turning on without power when they receive a toggle or set_color input. +- Fixed changing the amount of items to fabricate inadvertently starting(or activating) fabrication in MP if you've previously started fabricating something +- Fixed campaign settings resetting in the campaign setup menu every time you relaunch the game (meaning you'd always need to e.g. remember to toggle the tutorial off if you want to play without it). +- Fixed inverted mouse buttons not working properly since the last update: the left mouse button was considered the primary mouse button regardless of your OS settings. +- Fixed status monitor not properly displaying condition on tinkered items. +- Fixed machines smoking when above 100% condition with tinkering. +- Fixed inventory overlapping with the chatbox on low aspect ratios (small width, large height). +- Fixed some layering issues in abandoned outposts. +- Fixed water-sensitive items sometimes spawning as loot in wrecks. +- Fixed radio static still playing even if you don't have a headset. +- Fixed rifle grenade sounds not working. +- Fixed crashing on startup if the MD5 hash cache file is empty. +- Fixed research stations and loaders not being visible on the status monitor's electrical view. +- Fixed artifact missions sometimes choosing the same artifact as a target if you happen to have multiple missions active at a time, which would lead to console errors when the round ends. +- Fixed exosuit playing the warning beep if there's empty or almost empty tanks in any of its slots. +- Fixed oxygen generators deteriorating in some of the outpost modules. +- Fixed reputation loss when a character other than the player (e.g. crawlers in the 'crawleroutbreak' event) damages the outpost walls. +- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub. +- Fixed characters trying to walk in flooded spaces that are too low to stand in (like some of the tight passages in alien ruins). +- Genetic material backwards compatibility to fix old unidentified genetic materials disappearing from saves prior to v0.21.6.0. +- Fixed genetic materials being too rare in outposts now. +- Fixed hunting grounds affecting outposts 2 steps away, not just ones in adjacent locations. +- Fixed "residual waste" talent duplicating genetic materials. +- Fixed monsters sometimes spawning inside destructible ice chunks in caves. +- Fixed respawn shuttle sometimes spawning inside floating ice chunks. +- Fixed equipping a ranged weapon setting its reload timer to 1, making it possible to reduce some weapons' loading times by unequipping and equipping them. +- Fixed partially consumed items not staying on top of the stack they're in. +- Fixed submarine tier and class affecting the prices of the submarine upgrades: e.g. a tier 2 upgrade would cost more on a submarine where tier 2 is the maximum than on a submarine with a higher maximum. +- Fixed reputation loss when you steal items from bandits in a beacon station. +- Fixed equipped flares igniting when you click on the inventory. +- Fixed "Quickdraw" talent not affecting Alien Pistols. +- Fixed toolbelts and other items worn on the torso getting hidden when wearing a safety harness. +- Fixed advanced syringe gun and slipsuit fabrication recipes. +- Fixed floating pumps and ladder layering issues in Herja. +- Limited the number of makeshift shelves per sub to 3 (similar to portable pumps). Otherwise you can use them to expand the sub's cargo capacity indefinitely. +- Fixed dementonite and hardened crowbars spawning in respawn containers (= respawn shuttle cabinets). +- Fixed alien blood no longer causing psychosis, + made it slightly less effective to make fabricating blood packs more worthwhile +- Fixed fire extinguisher spray getting blocked by characters. +- Venture: Fixed the battery room not flooding properly (again), fixed the two hulls in the airlock not being linked, adjusted the waypoints a bit. +- Selkie: Disconnect the outer nodes from the ladder/door nodes, because the docking ports can't be opened manually. +- Fixed Thalamus AI not running properly when there's no player characters or submarines around (e.g. when all the players are in the freecam mode). +- Fixed the ammo indicator not showing correctly on the advanced syringe gun. +- Fixed bots sometimes getting confused by outside waypoints while being inside an outpost. +- Fixed item relocation logic running also on NPCs that are not in the player team, which could cause diving suits dropped by NPCs to get spawned in the player sub. + +Modding: +- Fixed crashing if a StatusEffect is configured to SpawnItemRandomly but doesn't configure anything to spawn. +- Improved the error handling of item/character variants. Previously if the parent prefab wasn't found, there was no error message, but the variant was still created, causing crashes in various situations. +- Added AITurretPriority and AISlowTurretPriority on items and characters. Setting the priority to 0 can be used for telling the bots to ignore the target entirely. Items also need to have IsAITurretTarget="True" enabled to make them a valid target. +- Added ItemDamageMultiplier on items. Can be used for increasing the damage caused by other items, like weapons. Works like the existing ExplosionDamageMultiplier. + --------------------------------------------------------------------------------------------------------- v0.21.6.0 --------------------------------------------------------------------------------------------------------- @@ -5,6 +420,7 @@ v0.21.6.0 - Minor localization fixes. - Fixed some occasional crashes in the character editor. + --------------------------------------------------------------------------------------------------------- v0.21.5.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/EndpointParseTests.cs b/Barotrauma/BarotraumaTest/EndpointParseTests.cs index c8ccb4e43..0038d952b 100644 --- a/Barotrauma/BarotraumaTest/EndpointParseTests.cs +++ b/Barotrauma/BarotraumaTest/EndpointParseTests.cs @@ -14,8 +14,7 @@ public class EndpointParseTests { Endpoint.Parse("127.0.0.1:27015") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), options => options.RespectingRuntimeTypes()); } @@ -25,8 +24,7 @@ public class EndpointParseTests { Endpoint.Parse("localhost:27015") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), options => options.RespectingRuntimeTypes()); } @@ -36,8 +34,7 @@ public class EndpointParseTests { Address.Parse("127.0.0.1") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option
.Some(new LidgrenAddress(IPAddress.Loopback)), options => options.RespectingRuntimeTypes()); } @@ -47,8 +44,7 @@ public class EndpointParseTests { Endpoint.Parse("STEAM_1:1:508792388") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new SteamP2PEndpoint(new SteamId(76561198977850505))), options => options.RespectingRuntimeTypes()); } @@ -58,8 +54,7 @@ public class EndpointParseTests { Address.Parse("STEAM_1:1:508792388") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option
.Some(new SteamP2PAddress(new SteamId(76561198977850505))), options => options.RespectingRuntimeTypes()); new SteamId(76561198977850505).StringRepresentation.Should().BeEquivalentTo("STEAM_1:1:508792388"); diff --git a/Barotrauma/BarotraumaTest/MathUtilsTests.cs b/Barotrauma/BarotraumaTest/MathUtilsTests.cs new file mode 100644 index 000000000..e624b3f4e --- /dev/null +++ b/Barotrauma/BarotraumaTest/MathUtilsTests.cs @@ -0,0 +1,67 @@ +using Barotrauma; +using FluentAssertions; +using Microsoft.Xna.Framework; +using System; +using Xunit; + +namespace TestProject; + +public class MathUtilsTests +{ + [Fact] + public void TestNearlyEquals() + { + MathUtils.NearlyEqual(0.0f, 0.0f).Should().BeTrue(); + MathUtils.NearlyEqual(-float.Epsilon, float.Epsilon).Should().BeTrue(); + MathUtils.NearlyEqual(0.1f + 0.2f, 0.3f).Should().BeTrue(); + MathUtils.NearlyEqual(-1.0f, 1.0f).Should().BeFalse(); + } + + [Fact] + public void TestWrapAngle() + { + MathUtils.NearlyEqual(MathUtils.WrapAnglePi(0.0f), 0.0f).Should().BeTrue(); + + CheckWrapAnglePiNearlyEqual(0, 0).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-90, -90).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-90, 90).Should().BeFalse(); + CheckWrapAnglePiNearlyEqual(-180, 180).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-190.0f, 170.0f).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-360, 0).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(360, 0).Should().BeTrue(); + + bool CheckWrapAnglePiNearlyEqual(float wrappedDeg, float deg) + { + float wrappedRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(wrappedDeg)); + float rad = MathHelper.ToRadians(deg); + return MathUtils.NearlyEqual(wrappedRad, rad) || MathUtils.NearlyEqual(Math.Abs(wrappedRad - rad), MathHelper.TwoPi); + } + + CheckWrapAngleTwoPiNearlyEqual(0, 0).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(90, 90).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(-90, 270).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(180, 180).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(360 * 5, 0).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(-360, 0).Should().BeTrue(); + + bool CheckWrapAngleTwoPiNearlyEqual(float wrappedDeg, float deg) + { + float wrappedRad = MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(wrappedDeg)); + float rad = MathHelper.ToRadians(deg); + return MathUtils.NearlyEqual(wrappedRad, rad) || MathUtils.NearlyEqual(Math.Abs(wrappedRad - rad), MathHelper.TwoPi); + } + + CheckShortestAngleNearlyEqual(0.0f, 0.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, 90.0f, 90.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, 360.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, -365.0f, -5.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(180.0f, -180.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(-355.0f, 5.0f, 10.0f); + + bool CheckShortestAngleNearlyEqual(float deg1, float deg2, float angle) + { + return MathUtils.NearlyEqual(MathUtils.GetShortestAngle(MathHelper.ToRadians(deg1), MathHelper.ToRadians(deg2)), MathHelper.ToRadians(angle)); + } + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/PropertyConditionalTests.cs b/Barotrauma/BarotraumaTest/PropertyConditionalTests.cs new file mode 100644 index 000000000..4c22c9dcd --- /dev/null +++ b/Barotrauma/BarotraumaTest/PropertyConditionalTests.cs @@ -0,0 +1,67 @@ +using Barotrauma; +using FluentAssertions; +using FsCheck; +using System; +using System.Collections.Immutable; +using System.Linq; +using Xunit; +namespace TestProject; + +public sealed class PropertyConditionalTests +{ + private readonly record struct OperatorStr(string Str); + private readonly record struct ConditionStr(string Str); + + private class CustomGenerators + { + public static Arbitrary OperatorStrGeneratorOverride() + { + return Gen.Choose(0, operators.Length-1) + .Select(i => operators[i]) + .ToArbitrary(); + } + + public static Arbitrary ConditionStrGeneratorOverride() + { + return Arb.Generate() + .Where(s => s != null && !s.Any(char.IsWhiteSpace) && !s.Contains(',')) + .Select(s => new ConditionStr(s)).ToArbitrary(); + } + } + + public PropertyConditionalTests() + { + Arb.Register(); + Arb.Register(); + } + + static ImmutableArray operators + = new[] + { + "eq", "neq", "gt", + "gte", "lt", "lte" + }.Select(s => new OperatorStr(s)).ToImmutableArray(); + + [Fact] + public void TestExtractComparisonOperatorFromConditionString() + { + Prop.ForAll( + Arb.Generate().ToArbitrary(), + Arb.Generate().ToArbitrary(), + ExtractComparisonOperatorFromConditionStringCase) + .QuickCheckThrowOnFailure(); + } + + private static void ExtractComparisonOperatorFromConditionStringCase(OperatorStr operatorStr, ConditionStr conditionStr) + { + var op = PropertyConditional.GetComparisonOperatorType(operatorStr.Str); + + var (op2, condStr) = PropertyConditional.ExtractComparisonOperatorFromConditionString(operatorStr.Str + " " + conditionStr.Str); + op2.Should().Be(op); + condStr.Should().Be(conditionStr.Str); + + var (op3, condStr2) = PropertyConditional.ExtractComparisonOperatorFromConditionString(conditionStr.Str); + op3.Should().Be(PropertyConditional.ComparisonOperatorType.Equals); + condStr2.Should().Be(conditionStr.Str); + } +} diff --git a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs index 12cdb9243..03cbcb402 100644 --- a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs +++ b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs @@ -37,22 +37,26 @@ public sealed class SerializableDateTimeTests { Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); } - + [Fact] public void ParseTest() { - var parseTest = "9369Y 09M 06D 03HR 43MIN 09SEC UTC+8:49"; - SerializableDateTime.Parse(parseTest); Prop.ForAll(ParseCheck).QuickCheckThrowOnFailure(); } - + + [Fact] + public void ToLocalTest() + { + Prop.ForAll(ToLocalCheck).QuickCheckThrowOnFailure(); + } + private static void EqualityCheck(SerializableDateTime original) { var local = original.ToLocal(); var utc = original.ToUtc(); - original.Should().BeEquivalentTo(local); - original.Should().BeEquivalentTo(utc); - local.Should().BeEquivalentTo(utc); + original.Should().BeEquivalentTo(local, because: "original must equal local"); + original.Should().BeEquivalentTo(utc, because: "original must equal utc"); + local.Should().BeEquivalentTo(utc, because: "local must equal utc"); } private static void ParseCheck(SerializableDateTime original) @@ -61,4 +65,11 @@ public sealed class SerializableDateTimeTests SerializableDateTime.Parse(str).TryUnwrap(out var parsedTime).Should().BeTrue(); parsedTime.Should().BeEquivalentTo(original); } + + private static void ToLocalCheck(SerializableDateTime original) + { + var localNow = SerializableDateTime.LocalNow; + var convertedDateTime = original.ToLocal(); + localNow.TimeZone.Should().BeEquivalentTo(convertedDateTime.TimeZone); + } } diff --git a/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs b/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs index fd6af2fa0..c47fdb10d 100644 --- a/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs +++ b/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs @@ -14,7 +14,7 @@ namespace Steamworks internal struct CallResult : INotifyCompletion where T : struct, ICallbackData { SteamAPICall_t call; - ISteamUtils utils; + ISteamUtils? utils; bool server; public CallResult( SteamAPICall_t call, bool server ) @@ -43,6 +43,8 @@ namespace Steamworks ///
public T? GetResult() { + if (utils is null) { return null; } + bool failed = false; if ( !utils.IsAPICallCompleted( call, ref failed ) || failed ) return null; @@ -76,6 +78,8 @@ namespace Steamworks { get { + if (utils is null) { return true; } + bool failed = false; if ( utils.IsAPICallCompleted( call, ref failed ) || failed ) return true; diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs index 28c80c246..aedff442f 100644 --- a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs @@ -4,7 +4,7 @@ namespace Steamworks { public class AuthTicket : IDisposable { - public byte[] Data; + public byte[]? Data; public uint Handle; public bool Canceled { get; private set; } @@ -17,7 +17,7 @@ namespace Steamworks { if (Handle != 0) { - SteamUser.Internal.CancelAuthTicket(Handle); + SteamUser.Internal?.CancelAuthTicket(Handle); } Handle = 0; diff --git a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs index 680c54238..a0a722464 100644 --- a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs +++ b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs @@ -26,7 +26,7 @@ namespace Steamworks /// Params are : [Callback Type] [Callback Contents] [server] /// ///
- public static Action OnDebugCallback; + public static Action? OnDebugCallback; /// /// Called if an exception happens during a callback/callresult. @@ -34,7 +34,7 @@ namespace Steamworks /// async.. and can fail silently. With this hooked you won't be stuck wondering /// what happened. /// - public static Action OnException; + public static Action? OnException; #region interop [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_Init", CallingConvention = CallingConvention.Cdecl )] @@ -288,7 +288,7 @@ namespace Steamworks /// /// Install a global callback. The passed function will get called if it's all good. /// - internal static void Install( Action p, bool server = false ) where T : ICallbackData + internal static void Install( Action p, bool server = false ) where T : struct, ICallbackData { var t = default( T ); var type = t.CallbackType; diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj index 474798281..26bfe8f64 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj @@ -5,10 +5,15 @@ $(DefineConstants);PLATFORM_POSIX64;PLATFORM_POSIX;PLATFORM_64 netstandard2.1 true - 8.0 + latest true false Steamworks + enable + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 94741525f..29d8dfcd1 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -10,6 +10,7 @@ true Steamworks AnyCPU;x64 + enable @@ -48,6 +49,10 @@ 1701;1702;1591;1587 + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs index baa03e68f..3e5e1c326 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs @@ -76,7 +76,7 @@ namespace Steamworks private static extern Utf8StringPointer _GetCurrentGameLanguage( IntPtr self ); #endregion - internal string GetCurrentGameLanguage() + internal string? GetCurrentGameLanguage() { var returnValue = _GetCurrentGameLanguage( Self ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs index 9adbeae02..b8c24ac7d 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs @@ -40,13 +40,13 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetResultItems", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[] pOutItemsArray, ref uint punOutItemsArraySize ); + private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ); #endregion /// /// Copies the contents of a result set into a flat array. The specific contents of the result set depend on which query which was used. /// - internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[] pOutItemsArray, ref uint punOutItemsArraySize ) + internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ) { var returnValue = _GetResultItems( Self, resultHandle, pOutItemsArray, ref punOutItemsArraySize ); return returnValue; @@ -55,10 +55,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetResultItemProperty", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetResultItemProperty( IntPtr self, SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); + private static extern bool _GetResultItemProperty( IntPtr self, SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); #endregion - internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) + internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { using var memory = Helpers.TakeMemory(); IntPtr mempchValueBuffer = memory; @@ -311,10 +311,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetItemDefinitionIDs", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetItemDefinitionIDs( IntPtr self, [In,Out] InventoryDefId[] pItemDefIDs, ref uint punItemDefIDsArraySize ); + private static extern bool _GetItemDefinitionIDs( IntPtr self, [In,Out] InventoryDefId[]? pItemDefIDs, ref uint punItemDefIDsArraySize ); #endregion - internal bool GetItemDefinitionIDs( [In,Out] InventoryDefId[] pItemDefIDs, ref uint punItemDefIDsArraySize ) + internal bool GetItemDefinitionIDs( [In,Out] InventoryDefId[]? pItemDefIDs, ref uint punItemDefIDsArraySize ) { var returnValue = _GetItemDefinitionIDs( Self, pItemDefIDs, ref punItemDefIDsArraySize ); return returnValue; @@ -323,10 +323,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetItemDefinitionProperty", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetItemDefinitionProperty( IntPtr self, InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); + private static extern bool _GetItemDefinitionProperty( IntPtr self, InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); #endregion - internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) + internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { using var memory = Helpers.TakeMemory(); IntPtr mempchValueBuffer = memory; diff --git a/Libraries/Facepunch.Steamworks/Networking/Connection.cs b/Libraries/Facepunch.Steamworks/Networking/Connection.cs index f5f75e5d2..0eb4aafce 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Connection.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Connection.cs @@ -24,7 +24,7 @@ namespace Steamworks.Data ///
public Result Accept() { - return SteamNetworkingSockets.Internal.AcceptConnection( this ); + return SteamNetworkingSockets.Internal?.AcceptConnection( this ) ?? Result.Fail; } /// @@ -33,7 +33,7 @@ namespace Steamworks.Data /// public bool Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" ) { - return SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger ); + return SteamNetworkingSockets.Internal != null && SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger ); } /// @@ -41,8 +41,8 @@ namespace Steamworks.Data /// public long UserData { - get => SteamNetworkingSockets.Internal.GetConnectionUserData( this ); - set => SteamNetworkingSockets.Internal.SetConnectionUserData( this, value ); + get => SteamNetworkingSockets.Internal?.GetConnectionUserData( this ) ?? 0; + set => SteamNetworkingSockets.Internal?.SetConnectionUserData( this, value ); } /// @@ -52,13 +52,13 @@ namespace Steamworks.Data { get { - if ( !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) ) + if ( SteamNetworkingSockets.Internal is null || !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) ) return "ERROR"; return strVal; } - set => SteamNetworkingSockets.Internal.SetConnectionName( this, value ); + set => SteamNetworkingSockets.Internal?.SetConnectionName( this, value ); } /// @@ -67,7 +67,7 @@ namespace Steamworks.Data public Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable ) { long messageNumber = 0; - return SteamNetworkingSockets.Internal.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ); + return SteamNetworkingSockets.Internal?.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ) ?? Result.Fail; } /// @@ -107,16 +107,16 @@ namespace Steamworks.Data /// Flush any messages waiting on the Nagle timer and send them at the next transmission /// opportunity (often that means right now). /// - public Result Flush() => SteamNetworkingSockets.Internal.FlushMessagesOnConnection( this ); + public Result Flush() => SteamNetworkingSockets.Internal?.FlushMessagesOnConnection( this ) ?? Result.Fail; /// /// Returns detailed connection stats in text format. Useful /// for dumping to a log, etc. /// /// Plain text connection info - public string DetailedStatus() + public string? DetailedStatus() { - if ( SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 ) + if ( SteamNetworkingSockets.Internal is null || SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 ) return null; return strVal; diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs index 2007cc777..1ac65e609 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs @@ -9,7 +9,7 @@ namespace Steamworks /// /// An optional interface to use instead of deriving /// - public IConnectionManager Interface { get; set; } + public IConnectionManager? Interface { get; set; } /// /// The actual connection we're managing @@ -94,6 +94,8 @@ namespace Steamworks public void Receive( int bufferSize = 32 ) { + if (SteamNetworkingSockets.Internal is null) { return; } + int processed = 0; IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); diff --git a/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs b/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs index 270bf5a9a..f118bdde0 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs @@ -107,10 +107,11 @@ namespace Steamworks.Data /// /// We override tostring to provide a sensible representation /// - public override string ToString() + public override string? ToString() { var id = this; - SteamNetworkingUtils.Internal.SteamNetworkingIdentity_ToString( ref id, out var str ); + string? str = null; + SteamNetworkingUtils.Internal?.SteamNetworkingIdentity_ToString( ref id, out str ); return str; } diff --git a/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs b/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs index 9aceca533..2fd3c926b 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs @@ -25,15 +25,16 @@ namespace Steamworks.Data public static NetPingLocation? TryParseFromString( string str ) { var result = default( NetPingLocation ); - if ( !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) ) + if ( SteamNetworkingUtils.Internal is null || !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) ) return null; return result; } - public override string ToString() + public override string? ToString() { - SteamNetworkingUtils.Internal.ConvertPingLocationToString( ref this, out var strVal ); + string? strVal = null; + SteamNetworkingUtils.Internal?.ConvertPingLocationToString( ref this, out strVal ); return strVal; } @@ -61,7 +62,7 @@ namespace Steamworks.Data /// You are looking for the "ticketgen" library. public int EstimatePingTo( NetPingLocation target ) { - return SteamNetworkingUtils.Internal.EstimatePingTimeBetweenTwoLocations( ref this, ref target ); + return SteamNetworkingUtils.Internal?.EstimatePingTimeBetweenTwoLocations( ref this, ref target ) ?? Defines.k_nSteamNetworkingPing_Failed; } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/Socket.cs b/Libraries/Facepunch.Steamworks/Networking/Socket.cs index 334f5893e..730e63324 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Socket.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Socket.cs @@ -1,4 +1,5 @@  +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Steamworks.Data @@ -17,10 +18,11 @@ namespace Steamworks.Data /// public bool Close() { - return SteamNetworkingSockets.Internal.CloseListenSocket( Id ); + return SteamNetworkingSockets.Internal != null && SteamNetworkingSockets.Internal.CloseListenSocket( Id ); } - public SocketManager Manager + [DisallowNull] + public SocketManager? Manager { get => SteamNetworkingSockets.GetSocketManager( Id ); set => SteamNetworkingSockets.SetSocketManager( Id, value ); diff --git a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs index 5c585faec..1d9721cf4 100644 --- a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs @@ -14,7 +14,7 @@ namespace Steamworks /// public partial class SocketManager { - public ISocketManager Interface { get; set; } + public ISocketManager? Interface { get; set; } public List Connecting = new List(); public List Connected = new List(); @@ -26,12 +26,12 @@ namespace Steamworks internal void Initialize() { - pollGroup = SteamNetworkingSockets.Internal.CreatePollGroup(); + pollGroup = SteamNetworkingSockets.Internal?.CreatePollGroup() ?? default; } public bool Close() { - if ( SteamNetworkingSockets.Internal.IsValid ) + if ( SteamNetworkingSockets.Internal is { IsValid: true } ) { SteamNetworkingSockets.Internal.DestroyPollGroup( pollGroup ); Socket.Close(); @@ -94,7 +94,7 @@ namespace Steamworks /// public virtual void OnConnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, pollGroup ); + SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, pollGroup ); Interface?.OnConnected( connection, info ); } @@ -104,7 +104,7 @@ namespace Steamworks ///
public virtual void OnDisconnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, 0 ); + SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, 0 ); connection.Close(); @@ -121,7 +121,7 @@ namespace Steamworks try { - processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize ); + processed = SteamNetworkingSockets.Internal?.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize ) ?? 0; for ( int i = 0; i < processed; i++ ) { diff --git a/Libraries/Facepunch.Steamworks/ServerList/Base.cs b/Libraries/Facepunch.Steamworks/ServerList/Base.cs index 9a5cb1125..eaf66449a 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Base.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Base.cs @@ -11,7 +11,7 @@ namespace Steamworks.ServerList { #region ISteamMatchmakingServers - internal static ISteamMatchmakingServers Internal => SteamMatchmakingServers.Internal; + internal static ISteamMatchmakingServers? Internal => SteamMatchmakingServers.Internal; #endregion @@ -23,17 +23,17 @@ namespace Steamworks.ServerList /// /// When a new server is added, this function will get called /// - public Action OnChanges; + public Action? OnChanges; /// /// Called for every responsive server /// - public Action OnResponsiveServer; + public Action? OnResponsiveServer; /// /// Called for every unresponsive server /// - public Action OnUnresponsiveServer; + public Action? OnUnresponsiveServer; /// /// A list of servers that responded. If you're only interested in servers that responded since you @@ -98,7 +98,7 @@ namespace Steamworks.ServerList return true; } - public virtual void Cancel() => Internal.CancelQuery( request ); + public virtual void Cancel() => Internal?.CancelQuery( request ); // Overrides internal abstract void LaunchQuery(); @@ -117,8 +117,8 @@ namespace Steamworks.ServerList #endregion - internal int Count => Internal.GetServerCount( request ); - internal bool IsRefreshing => request.Value != IntPtr.Zero && Internal.IsRefreshing( request ); + internal int Count => Internal?.GetServerCount( request ) ?? 0; + internal bool IsRefreshing => request.Value != IntPtr.Zero && Internal != null && Internal.IsRefreshing( request ); internal List watchList = new List(); internal int LastCount = 0; @@ -134,7 +134,7 @@ namespace Steamworks.ServerList if ( request.Value != IntPtr.Zero ) { Cancel(); - Internal.ReleaseRequest( request ); + Internal?.ReleaseRequest( request ); request = IntPtr.Zero; } } @@ -166,6 +166,8 @@ namespace Steamworks.ServerList { watchList.RemoveAll( x => { + if (Internal is null) { return true; } + var info = Internal.GetServerDetails( request, x ); if ( info.HadSuccessfulResponse ) { @@ -181,6 +183,8 @@ namespace Steamworks.ServerList { watchList.RemoveAll( x => { + if (Internal is null) { return true; } + var info = Internal.GetServerDetails( request, x ); OnServer( ServerInfo.From( info ), info.HadSuccessfulResponse ); return true; diff --git a/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs b/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs index 6f1ffa3c0..e71394570 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestFavoritesServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/Friends.cs b/Libraries/Facepunch.Steamworks/ServerList/Friends.cs index eb66a692b..60d72e31c 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Friends.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Friends.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestFriendsServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/History.cs b/Libraries/Facepunch.Steamworks/ServerList/History.cs index 55ccc166c..3d059767e 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/History.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/History.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestHistoryServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/Internet.cs b/Libraries/Facepunch.Steamworks/ServerList/Internet.cs index 86e599e66..c493b26fa 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Internet.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Internet.cs @@ -10,8 +10,8 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); - request = Internal.RequestInternetServerList( AppId.Value, filters, (uint)filters.Length, IntPtr.Zero ); } } diff --git a/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs b/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs index 746887335..ed5d476c8 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } request = Internal.RequestLANServerList( AppId.Value, IntPtr.Zero ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamApps.cs b/Libraries/Facepunch.Steamworks/SteamApps.cs index c201882b9..80af70a5e 100644 --- a/Libraries/Facepunch.Steamworks/SteamApps.cs +++ b/Libraries/Facepunch.Steamworks/SteamApps.cs @@ -13,7 +13,7 @@ namespace Steamworks /// public class SteamApps : SteamSharedClass { - internal static ISteamApps Internal => Interface as ISteamApps; + internal static ISteamApps? Internal => Interface as ISteamApps; internal override void InitializeInterface( bool server ) { @@ -29,7 +29,7 @@ namespace Steamworks /// /// posted after the user gains ownership of DLC and that DLC is installed /// - public static event Action OnDlcInstalled; + public static event Action? OnDlcInstalled; /// /// posted after the user gains executes a Steam URL with command line or query parameters @@ -37,61 +37,63 @@ namespace Steamworks /// while the game is already running. The new params can be queried /// with GetLaunchQueryParam and GetLaunchCommandLine /// - public static event Action OnNewLaunchParameters; + public static event Action? OnNewLaunchParameters; /// /// Checks if the active user is subscribed to the current App ID /// - public static bool IsSubscribed => Internal.BIsSubscribed(); + public static bool IsSubscribed => Internal != null && Internal.BIsSubscribed(); /// /// Check if user borrowed this game via Family Sharing, If true, call GetAppOwner() to get the lender SteamID /// - public static bool IsSubscribedFromFamilySharing => Internal.BIsSubscribedFromFamilySharing(); + public static bool IsSubscribedFromFamilySharing => Internal != null && Internal.BIsSubscribedFromFamilySharing(); /// /// Checks if the license owned by the user provides low violence depots. /// Low violence depots are useful for copies sold in countries that have content restrictions /// - public static bool IsLowViolence => Internal.BIsLowViolence(); + public static bool IsLowViolence => Internal != null && Internal.BIsLowViolence(); /// /// Checks whether the current App ID license is for Cyber Cafes. /// - public static bool IsCybercafe => Internal.BIsCybercafe(); + public static bool IsCybercafe => Internal != null && Internal.BIsCybercafe(); /// /// CChecks if the user has a VAC ban on their account /// - public static bool IsVACBanned => Internal.BIsVACBanned(); + public static bool IsVACBanned => Internal != null && Internal.BIsVACBanned(); /// /// Gets the current language that the user has set. /// This falls back to the Steam UI language if the user hasn't explicitly picked a language for the title. /// - public static string GameLanguage => Internal.GetCurrentGameLanguage(); + public static string? GameLanguage => Internal?.GetCurrentGameLanguage(); /// /// Gets a list of the languages the current app supports. /// - public static string[] AvailableLanguages => Internal.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); + public static string[]? AvailableLanguages => Internal?.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); /// /// Checks if the active user is subscribed to a specified AppId. /// Only use this if you need to check ownership of another game related to yours, a demo for example. /// - public static bool IsSubscribedToApp( AppId appid ) => Internal.BIsSubscribedApp( appid.Value ); + public static bool IsSubscribedToApp( AppId appid ) => Internal != null && Internal.BIsSubscribedApp( appid.Value ); /// /// Checks if the user owns a specific DLC and if the DLC is installed /// - public static bool IsDlcInstalled( AppId appid ) => Internal.BIsDlcInstalled( appid.Value ); + public static bool IsDlcInstalled( AppId appid ) => Internal != null && Internal.BIsDlcInstalled( appid.Value ); /// /// Returns the time of the purchase of the app /// public static DateTime PurchaseTime( AppId appid = default ) { + if (Internal is null) { return default; } + if ( appid == 0 ) appid = SteamClient.AppId; @@ -103,7 +105,7 @@ namespace Steamworks /// This function will return false for users who have a retail or other type of license /// Before using, please ask your Valve technical contact how to package and secure your free weekened ///
- public static bool IsSubscribedFromFreeWeekend => Internal.BIsSubscribedFromFreeWeekend(); + public static bool IsSubscribedFromFreeWeekend => Internal != null && Internal.BIsSubscribedFromFreeWeekend(); /// /// Returns metadata for all available DLC @@ -113,8 +115,12 @@ namespace Steamworks var appid = default( AppId ); var available = false; - for ( int i = 0; i < Internal.GetDLCCount(); i++ ) + if (Internal is null) { yield break; } + + int dlcCount = Internal.GetDLCCount(); + for ( int i = 0; i < dlcCount; i++ ) { + if (Internal is null) { yield break; } if ( !Internal.BGetDLCDataByIndex( i, ref appid, ref available, out var strVal ) ) continue; @@ -130,21 +136,21 @@ namespace Steamworks /// /// Install/Uninstall control for optional DLC /// - public static void InstallDlc( AppId appid ) => Internal.InstallDLC( appid.Value ); + public static void InstallDlc( AppId appid ) => Internal?.InstallDLC( appid.Value ); /// /// Install/Uninstall control for optional DLC /// - public static void UninstallDlc( AppId appid ) => Internal.UninstallDLC( appid.Value ); + public static void UninstallDlc( AppId appid ) => Internal?.UninstallDLC( appid.Value ); /// /// Returns null if we're not on a beta branch, else the name of the branch /// - public static string CurrentBetaName + public static string? CurrentBetaName { get { - if ( !Internal.GetCurrentBetaName( out var strVal ) ) + if ( Internal is null || !Internal.GetCurrentBetaName( out var strVal ) ) return null; return strVal; @@ -157,13 +163,15 @@ namespace Steamworks /// If you detect the game is out-of-date(for example, by having the client detect a version mismatch with a server), /// you can call use MarkContentCorrupt to force a verify, show a message to the user, and then quit. /// - public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal.MarkContentCorrupt( missingFilesOnly ); + public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal?.MarkContentCorrupt( missingFilesOnly ); /// /// Gets a list of all installed depots for a given App ID in mount order /// public static IEnumerable InstalledDepots( AppId appid = default ) { + if (Internal is null) { yield break; } + if ( appid == 0 ) appid = SteamClient.AppId; @@ -180,12 +188,12 @@ namespace Steamworks /// Gets the install folder for a specific AppID. /// This works even if the application is not installed, based on where the game would be installed with the default Steam library location. ///
- public static string AppInstallDir( AppId appid = default ) + public static string? AppInstallDir( AppId appid = default ) { if ( appid == 0 ) appid = SteamClient.AppId; - if ( Internal.GetAppInstallDir( appid.Value, out var strVal ) == 0 ) + if ( Internal is null || Internal.GetAppInstallDir( appid.Value, out var strVal ) == 0 ) return null; return strVal; @@ -194,12 +202,12 @@ namespace Steamworks /// /// The app may not actually be owned by the current user, they may have it left over from a free weekend, etc. /// - public static bool IsAppInstalled( AppId appid ) => Internal.BIsAppInstalled( appid.Value ); + public static bool IsAppInstalled( AppId appid ) => Internal != null && Internal.BIsAppInstalled( appid.Value ); /// /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed.. /// - public static SteamId AppOwner => Internal.GetAppOwner().Value; + public static SteamId AppOwner => Internal?.GetAppOwner().Value ?? default; /// /// Gets the associated launch parameter if the game is run via steam://run/appid/?param1=value1;param2=value2;param3=value3 etc. @@ -207,7 +215,7 @@ namespace Steamworks /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, /// but it is advised that you not param names beginning with an underscore for your own features. /// - public static string GetLaunchParam( string param ) => Internal.GetLaunchQueryParam( param ); + public static string? GetLaunchParam( string param ) => Internal?.GetLaunchQueryParam( param ); /// /// Gets the download progress for optional DLC. @@ -217,7 +225,7 @@ namespace Steamworks ulong punBytesDownloaded = 0; ulong punBytesTotal = 0; - if ( !Internal.GetDlcDownloadProgress( appid.Value, ref punBytesDownloaded, ref punBytesTotal ) ) + if ( Internal is null || !Internal.GetDlcDownloadProgress( appid.Value, ref punBytesDownloaded, ref punBytesTotal ) ) return default; return new DownloadProgress { BytesDownloaded = punBytesDownloaded, BytesTotal = punBytesTotal, Active = true }; @@ -227,7 +235,7 @@ namespace Steamworks /// Gets the buildid of this app, may change at any time based on backend updates to the game. /// Defaults to 0 if you're not running a build downloaded from steam. /// - public static int BuildId => Internal.GetAppBuildId(); + public static int BuildId => Internal?.GetAppBuildId() ?? 0; /// @@ -236,6 +244,7 @@ namespace Steamworks /// public static async Task GetFileDetailsAsync( string filename ) { + if (Internal is null) { return null; } var r = await Internal.GetFileDetails( filename ); if ( !r.HasValue || r.Value.Result != Result.OK ) @@ -257,11 +266,12 @@ namespace Steamworks /// path and not be placed on the OS command line, you must set a value in your app's /// configuration on Steam. Ask Valve for help with this. ///
- public static string CommandLine + public static string? CommandLine { get { - Internal.GetLaunchCommandLine( out var strVal ); + string? strVal = null; + Internal?.GetLaunchCommandLine( out strVal ); return strVal; } } diff --git a/Libraries/Facepunch.Steamworks/SteamClient.cs b/Libraries/Facepunch.Steamworks/SteamClient.cs index fb44cff44..31ef55045 100644 --- a/Libraries/Facepunch.Steamworks/SteamClient.cs +++ b/Libraries/Facepunch.Steamworks/SteamClient.cs @@ -124,7 +124,7 @@ namespace Steamworks /// very good experience for the player and you could be preventing them from accessing APIs that do not /// need a live connection to Steam. /// - public static bool IsLoggedOn => SteamUser.Internal.BLoggedOn(); + public static bool IsLoggedOn => SteamUser.Internal != null && SteamUser.Internal.BLoggedOn(); /// /// Gets the Steam ID of the account currently logged into the Steam client. This is @@ -132,18 +132,18 @@ namespace Steamworks /// A Steam ID is a unique identifier for a Steam accounts, Steam groups, Lobbies and Chat /// rooms, and used to differentiate users in all parts of the Steamworks API. /// - public static SteamId SteamId => SteamUser.Internal.GetSteamID(); + public static SteamId SteamId => SteamUser.Internal?.GetSteamID() ?? default; /// /// returns the local players name - guaranteed to not be NULL. /// this is the same name as on the users community profile page /// - public static string Name => SteamFriends.Internal.GetPersonaName(); + public static string? Name => SteamFriends.Internal?.GetPersonaName(); /// /// gets the status of the current user /// - public static FriendState State => SteamFriends.Internal.GetPersonaState(); + public static FriendState State => SteamFriends.Internal?.GetPersonaState() ?? FriendState.Offline; /// /// returns the appID of the current process diff --git a/Libraries/Facepunch.Steamworks/SteamFriends.cs b/Libraries/Facepunch.Steamworks/SteamFriends.cs index 36a57e6cc..bcc457525 100644 --- a/Libraries/Facepunch.Steamworks/SteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/SteamFriends.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamFriends : SteamClientClass { - internal static ISteamFriends Internal => Interface as ISteamFriends; + internal static ISteamFriends? Internal => Interface as ISteamFriends; internal override void InitializeInterface( bool server ) { @@ -23,7 +23,7 @@ namespace Steamworks InstallEvents(); } - static Dictionary richPresence; + static Dictionary? richPresence; internal void InstallEvents() { @@ -40,42 +40,42 @@ namespace Steamworks /// Called when chat message has been received from a friend. You'll need to turn on /// ListenForFriendsMessages to recieve this. (friend, msgtype, message) /// - public static event Action OnChatMessage; + public static event Action? OnChatMessage; /// /// called when a friends' status changes /// - public static event Action OnPersonaStateChange; + public static event Action? OnPersonaStateChange; /// /// Called when the user tries to join a game from their friends list /// rich presence will have been set with the "connect" key which is set here /// - public static event Action OnGameRichPresenceJoinRequested; + public static event Action? OnGameRichPresenceJoinRequested; /// /// Posted when game overlay activates or deactivates /// the game can use this to be pause or resume single player games /// - public static event Action OnGameOverlayActivated; + public static event Action? OnGameOverlayActivated; /// /// Called when the user tries to join a different game server from their friends list /// game client should attempt to connect to specified server when this is received /// - public static event Action OnGameServerChangeRequested; + public static event Action? OnGameServerChangeRequested; /// /// Called when the user tries to join a lobby from their friends list /// game client should attempt to connect to specified lobby when this is received /// - public static event Action OnGameLobbyJoinRequested; + public static event Action? OnGameLobbyJoinRequested; /// /// Callback indicating updated data about friends rich presence information /// - public static event Action OnFriendRichPresenceUpdate; + public static event Action? OnFriendRichPresenceUpdate; static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data ) { @@ -86,7 +86,7 @@ namespace Steamworks using var buffer = Helpers.TakeMemory(); var type = ChatEntryType.ChatMsg; - var len = Internal.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ); + var len = Internal?.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ) ?? 0; if ( len == 0 && type == ChatEntryType.Invalid ) return; @@ -99,15 +99,18 @@ namespace Steamworks private static IEnumerable GetFriendsWithFlag(FriendFlags flag) { - for ( int i=0; i GetFriends() @@ -142,24 +145,33 @@ namespace Steamworks public static IEnumerable GetPlayedWith() { - for ( int i = 0; i < Internal.GetCoplayFriendCount(); i++ ) + if (Internal is null) { yield break; } + int friendCount = Internal.GetCoplayFriendCount(); + for ( int i = 0; i < friendCount; i++ ) { + if (Internal is null) { yield break; } yield return new Friend( Internal.GetCoplayFriend( i ) ); } } public static IEnumerable GetFromSource( SteamId steamid ) { - for ( int i = 0; i < Internal.GetFriendCountFromSource( steamid ); i++ ) - { + if (Internal is null) { yield break; } + int friendCount = Internal.GetFriendCountFromSource( steamid ); + for ( int i = 0; i < friendCount; i++ ) + { + if (Internal is null) { yield break; } yield return new Friend( Internal.GetFriendFromSourceByIndex( steamid, i ) ); } } public static IEnumerable GetClans() { - for (int i = 0; i < Internal.GetClanCount(); i++) + if (Internal is null) { yield break; } + int friendCount = Internal.GetClanCount(); + for ( int i = 0; i < friendCount; i++ ) { + if (Internal is null) { yield break; } yield return new Clan( Internal.GetClanByIndex( i ) ); } } @@ -174,7 +186,7 @@ namespace Steamworks /// "stats", /// "achievements". /// - public static void OpenOverlay( string type ) => Internal.ActivateGameOverlay( type ); + public static void OpenOverlay( string type ) => Internal?.ActivateGameOverlay( type ); /// /// "steamid" - Opens the overlay web browser to the specified user or groups profile. @@ -187,35 +199,35 @@ namespace Steamworks /// "friendrequestaccept" - Opens the overlay in minimal mode prompting the user to accept an incoming friend invite. /// "friendrequestignore" - Opens the overlay in minimal mode prompting the user to ignore an incoming friend invite. /// - public static void OpenUserOverlay( SteamId id, string type ) => Internal.ActivateGameOverlayToUser( type, id ); + public static void OpenUserOverlay( SteamId id, string type ) => Internal?.ActivateGameOverlayToUser( type, id ); /// /// Activates the Steam Overlay to the Steam store page for the provided app. /// - public static void OpenStoreOverlay( AppId id ) => Internal.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); + public static void OpenStoreOverlay( AppId id ) => Internal?.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); /// /// Activates Steam Overlay web browser directly to the specified URL. /// - public static void OpenWebOverlay( string url, bool modal = false ) => Internal.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); + public static void OpenWebOverlay( string url, bool modal = false ) => Internal?.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); /// /// Activates the Steam Overlay to open the invite dialog. Invitations sent from this dialog will be for the provided lobby. /// - public static void OpenGameInviteOverlay( SteamId lobby ) => Internal.ActivateGameOverlayInviteDialog( lobby ); + public static void OpenGameInviteOverlay( SteamId lobby ) => Internal?.ActivateGameOverlayInviteDialog( lobby ); /// /// Mark a target user as 'played with'. /// NOTE: The current user must be in game with the other player for the association to work. /// - public static void SetPlayedWith( SteamId steamid ) => Internal.SetPlayedWith( steamid ); + public static void SetPlayedWith( SteamId steamid ) => Internal?.SetPlayedWith( steamid ); /// /// Requests the persona name and optionally the avatar of a specified user. /// NOTE: It's a lot slower to download avatars and churns the local cache, so if you don't need avatars, don't request them. /// returns true if we're fetching the data, false if we already have it /// - public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal.RequestUserInformation( steamid, nameonly ); + public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal != null && Internal.RequestUserInformation( steamid, nameonly ); internal static async Task CacheUserInformationAsync( SteamId steamid, bool nameonly ) @@ -239,18 +251,21 @@ namespace Steamworks public static async Task GetSmallAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) ); } public static async Task GetMediumAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) ); } public static async Task GetLargeAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); var imageid = Internal.GetLargeFriendAvatar( steamid ); @@ -268,8 +283,10 @@ namespace Steamworks /// /// Find a rich presence value by key for current user. Will be null if not found. /// - public static string GetRichPresence( string key ) + public static string? GetRichPresence( string key ) { + if (richPresence is null) { return null; } + if ( richPresence.TryGetValue( key, out var val ) ) return val; @@ -281,6 +298,8 @@ namespace Steamworks /// public static bool SetRichPresence( string key, string value ) { + if (richPresence is null || Internal is null) { return false; } + bool success = Internal.SetRichPresence( key, value ); if ( success ) @@ -294,8 +313,8 @@ namespace Steamworks /// public static void ClearRichPresence() { - richPresence.Clear(); - Internal.ClearRichPresence(); + richPresence?.Clear(); + Internal?.ClearRichPresence(); } static bool _listenForFriendsMessages; @@ -312,20 +331,22 @@ namespace Steamworks set { _listenForFriendsMessages = value; - Internal.SetListenForFriendsMessages( value ); + Internal?.SetListenForFriendsMessages( value ); } } public static async Task IsFollowing(SteamId steamID) { + if (Internal is null) { return false; } var r = await Internal.IsFollowing(steamID); - return r.Value.IsFollowing; + return r?.IsFollowing ?? false; } public static async Task GetFollowerCount(SteamId steamID) { + if (Internal is null) { return 0; } var r = await Internal.GetFollowerCount(steamID); - return r.Value.Count; + return r?.Count ?? 0; } public static async Task GetFollowingList() @@ -337,6 +358,7 @@ namespace Steamworks do { + if (Internal is null) { break; } if ( (result = await Internal.EnumerateFollowingList((uint)resultCount)) != null) { resultCount += result.Value.ResultsReturned; diff --git a/Libraries/Facepunch.Steamworks/SteamInput.cs b/Libraries/Facepunch.Steamworks/SteamInput.cs index d55e523c4..3db2293b4 100644 --- a/Libraries/Facepunch.Steamworks/SteamInput.cs +++ b/Libraries/Facepunch.Steamworks/SteamInput.cs @@ -5,7 +5,7 @@ namespace Steamworks { public class SteamInput : SteamClientClass { - internal static ISteamInput Internal => Interface as ISteamInput; + internal static ISteamInput? Internal => Interface as ISteamInput; internal override void InitializeInterface( bool server ) { @@ -22,7 +22,7 @@ namespace Steamworks /// public static void RunFrame() { - Internal.RunFrame(); + Internal?.RunFrame(); } static readonly InputHandle_t[] queryArray = new InputHandle_t[STEAM_CONTROLLER_MAX_COUNT]; @@ -34,7 +34,7 @@ namespace Steamworks { get { - var num = Internal.GetConnectedControllers( queryArray ); + var num = Internal?.GetConnectedControllers( queryArray ) ?? 0; for ( int i = 0; i < num; i++ ) { @@ -52,8 +52,10 @@ namespace Steamworks /// /// /// - public static string GetDigitalActionGlyph( Controller controller, string action ) + public static string? GetDigitalActionGlyph( Controller controller, string action ) { + if (Internal is null) { return null; } + InputActionOrigin origin = InputActionOrigin.None; Internal.GetDigitalActionOrigins( @@ -69,6 +71,8 @@ namespace Steamworks internal static Dictionary DigitalHandles = new Dictionary(); internal static InputDigitalActionHandle_t GetDigitalActionHandle( string name ) { + if (Internal is null) { return default; } + if ( DigitalHandles.TryGetValue( name, out var val ) ) return val; @@ -80,6 +84,8 @@ namespace Steamworks internal static Dictionary AnalogHandles = new Dictionary(); internal static InputAnalogActionHandle_t GetAnalogActionHandle( string name ) { + if (Internal is null) { return default; } + if ( AnalogHandles.TryGetValue( name, out var val ) ) return val; @@ -91,6 +97,8 @@ namespace Steamworks internal static Dictionary ActionSets = new Dictionary(); internal static InputActionSetHandle_t GetActionSetHandle( string name ) { + if (Internal is null) { return default; } + if ( ActionSets.TryGetValue( name, out var val ) ) return val; diff --git a/Libraries/Facepunch.Steamworks/SteamInventory.cs b/Libraries/Facepunch.Steamworks/SteamInventory.cs index f0a63c040..f45131dd1 100644 --- a/Libraries/Facepunch.Steamworks/SteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/SteamInventory.cs @@ -14,7 +14,7 @@ namespace Steamworks /// public class SteamInventory : SteamSharedClass { - internal static ISteamInventory Internal => Interface as ISteamInventory; + internal static ISteamInventory? Internal => Interface as ISteamInventory; internal override void InitializeInterface( bool server ) { @@ -41,8 +41,8 @@ namespace Steamworks OnInventoryUpdated?.Invoke( r ); } - public static event Action OnInventoryUpdated; - public static event Action OnDefinitionsUpdated; + public static event Action? OnInventoryUpdated; + public static event Action? OnDefinitionsUpdated; static void LoadDefinitions() { @@ -79,7 +79,7 @@ namespace Steamworks LoadDefinitions(); } - Internal.LoadItemDefinitions(); + Internal?.LoadItemDefinitions(); } /// @@ -113,7 +113,7 @@ namespace Steamworks /// Try to find the definition that matches this definition ID. /// Uses a dictionary so should be about as fast as possible. /// - public static InventoryDef FindDefinition( InventoryDefId defId ) + public static InventoryDef? FindDefinition( InventoryDefId defId ) { if ( _defMap == null ) return null; @@ -124,15 +124,17 @@ namespace Steamworks return null; } - public static string Currency { get; internal set; } + public static string Currency { get; internal set; } = ""; - public static async Task GetDefinitionsWithPricesAsync() + public static async Task GetDefinitionsWithPricesAsync() { + if (Internal is null) { return null; } + var priceRequest = await Internal.RequestPrices(); - if ( !priceRequest.HasValue || priceRequest.Value.Result != Result.OK ) + if ( priceRequest?.Result != Result.OK ) return null; - Currency = priceRequest?.CurrencyUTF8(); + Currency = priceRequest.Value.CurrencyUTF8(); var num = Internal.GetNumItemsWithPrices(); @@ -153,15 +155,15 @@ namespace Steamworks /// /// We will try to keep this list of your items automatically up to date. /// - public static InventoryItem[] Items { get; internal set; } + public static InventoryItem[]? Items { get; internal set; } - public static InventoryDef[] Definitions { get; internal set; } - static Dictionary _defMap; + public static InventoryDef[]? Definitions { get; internal set; } + static Dictionary? _defMap; - internal static InventoryDef[] GetDefinitions() + internal static InventoryDef[]? GetDefinitions() { uint num = 0; - if ( !Internal.GetItemDefinitionIDs( null, ref num ) ) + if ( Internal is null || !Internal.GetItemDefinitionIDs( null, ref num ) ) return null; var defs = new InventoryDefId[num]; @@ -178,7 +180,7 @@ namespace Steamworks public static bool GetAllItems() { var sresult = Defines.k_SteamInventoryResultInvalid; - return Internal.GetAllItems( ref sresult ); + return Internal != null && Internal.GetAllItems( ref sresult ); } /// @@ -188,7 +190,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.GetAllItems( ref sresult ) ) + if ( Internal is null || !Internal.GetAllItems( ref sresult ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -207,7 +209,7 @@ namespace Steamworks var defs = new InventoryDefId[] { target.Id }; var cnts = new uint[] { (uint)amount }; - if ( !Internal.GenerateItems( ref sresult, defs, cnts, 1 ) ) + if ( Internal is null || !Internal.GenerateItems( ref sresult, defs, cnts, 1 ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -228,7 +230,7 @@ namespace Steamworks var sell = list.Select( x => x.Id ).ToArray(); var sellc = list.Select( x => (uint)1 ).ToArray(); - if ( !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) + if ( Internal is null || !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -249,7 +251,7 @@ namespace Steamworks var sell = list.Select( x => x.Item.Id ).ToArray(); var sellc = list.Select( x => (uint) x.Quantity ).ToArray(); - if ( !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) + if ( Internal is null || !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -285,7 +287,7 @@ namespace Steamworks var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.DeserializeResult( ref sresult, (IntPtr)ptr, (uint)dataLength, false ) ) + if ( Internal is null || !Internal.DeserializeResult( ref sresult, (IntPtr)ptr, (uint)dataLength, false ) ) return null; @@ -306,7 +308,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.GrantPromoItems( ref sresult ) ) + if ( Internal is null || !Internal.GrantPromoItems( ref sresult ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -320,7 +322,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.TriggerItemDrop( ref sresult, id ) ) + if ( Internal is null || !Internal.TriggerItemDrop( ref sresult, id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -334,7 +336,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.AddPromoItem( ref sresult, id ) ) + if ( Internal is null || !Internal.AddPromoItem( ref sresult, id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -347,6 +349,8 @@ namespace Steamworks /// public static async Task StartPurchaseAsync( InventoryDef[] items ) { + if (Internal is null) { return null; } + var item_i = items.Select( x => x._id ).ToArray(); var item_q = items.Select( x => (uint)1 ).ToArray(); diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index ff3857a6f..df3ac42eb 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamMatchmaking : SteamClientClass { - internal static ISteamMatchmaking Internal => Interface as ISteamMatchmaking; + internal static ISteamMatchmaking? Internal => Interface as ISteamMatchmaking; internal override void InitializeInterface( bool server ) { @@ -70,6 +70,8 @@ namespace Steamworks static private unsafe void OnLobbyChatMessageRecievedAPI( LobbyChatMsg_t callback ) { + if (Internal is null) { return; } + SteamId steamid = default; ChatEntryType chatEntryType = default; using var buffer = Helpers.TakeMemory(); @@ -101,62 +103,62 @@ namespace Steamworks /// /// Someone invited you to a lobby /// - public static event Action OnLobbyInvite; + public static event Action? OnLobbyInvite; /// /// You joined a lobby /// - public static event Action OnLobbyEntered; + public static event Action? OnLobbyEntered; /// /// You created a lobby /// - public static event Action OnLobbyCreated; + public static event Action? OnLobbyCreated; /// /// A game server has been associated with the lobby /// - public static event Action OnLobbyGameCreated; + public static event Action? OnLobbyGameCreated; /// /// The lobby metadata has changed /// - public static event Action OnLobbyDataChanged; + public static event Action? OnLobbyDataChanged; /// /// The lobby member metadata has changed /// - public static event Action OnLobbyMemberDataChanged; + public static event Action? OnLobbyMemberDataChanged; /// /// The lobby member joined /// - public static event Action OnLobbyMemberJoined; + public static event Action? OnLobbyMemberJoined; /// /// The lobby member left the room /// - public static event Action OnLobbyMemberLeave; + public static event Action? OnLobbyMemberLeave; /// /// The lobby member left the room /// - public static event Action OnLobbyMemberDisconnected; + public static event Action? OnLobbyMemberDisconnected; /// /// The lobby member was kicked. The 3rd param is the user that kicked them. /// - public static event Action OnLobbyMemberKicked; + public static event Action? OnLobbyMemberKicked; /// /// The lobby member was banned. The 3rd param is the user that banned them. /// - public static event Action OnLobbyMemberBanned; + public static event Action? OnLobbyMemberBanned; /// /// A chat message was recieved from a member of a lobby /// - public static event Action OnChatMessage; + public static event Action? OnChatMessage; public static LobbyQuery CreateLobbyQuery() { return new LobbyQuery(); } @@ -165,6 +167,8 @@ namespace Steamworks /// public static async Task CreateLobbyAsync( int maxMembers = 100 ) { + if (Internal is null) { return null; } + var lobby = await Internal.CreateLobby( LobbyType.Invisible, maxMembers ); if ( !lobby.HasValue ) { return null; } @@ -176,6 +180,8 @@ namespace Steamworks /// public static async Task JoinLobbyAsync( SteamId lobbyId ) { + if (Internal is null) { return null; } + var lobby = await Internal.JoinLobby( lobbyId ); if ( !lobby.HasValue ) return null; @@ -187,6 +193,8 @@ namespace Steamworks /// public static IEnumerable GetFavoriteServers() { + if (Internal is null) { yield break; } + var count = Internal.GetFavoriteGameCount(); for( int i=0; i public static IEnumerable GetHistoryServers() { + if (Internal is null) { yield break; } + var count = Internal.GetFavoriteGameCount(); for ( int i = 0; i < count; i++ ) diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs index c1af07aee..a1d7d2c71 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs @@ -12,7 +12,7 @@ namespace Steamworks /// internal class SteamMatchmakingServers : SteamClientClass { - internal static ISteamMatchmakingServers Internal => Interface as ISteamMatchmakingServers; + internal static ISteamMatchmakingServers? Internal => Interface as ISteamMatchmakingServers; internal override void InitializeInterface( bool server ) { diff --git a/Libraries/Facepunch.Steamworks/SteamMusic.cs b/Libraries/Facepunch.Steamworks/SteamMusic.cs index 1ef38f8bd..b0d6ef42d 100644 --- a/Libraries/Facepunch.Steamworks/SteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/SteamMusic.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamMusic : SteamClientClass { - internal static ISteamMusic Internal => Interface as ISteamMusic; + internal static ISteamMusic? Internal => Interface as ISteamMusic; internal override void InitializeInterface( bool server ) { @@ -33,50 +33,50 @@ namespace Steamworks /// /// Playback status changed /// - public static event Action OnPlaybackChanged; + public static event Action? OnPlaybackChanged; /// /// Volume changed, parameter is new volume /// - public static event Action OnVolumeChanged; + public static event Action? OnVolumeChanged; /// /// Checks if Steam Music is enabled /// - public static bool IsEnabled => Internal.BIsEnabled(); + public static bool IsEnabled => Internal != null && Internal.BIsEnabled(); /// /// true if a song is currently playing, paused, or queued up to play; otherwise false. /// - public static bool IsPlaying => Internal.BIsPlaying(); + public static bool IsPlaying => Internal != null && Internal.BIsPlaying(); /// /// Gets the current status of the Steam Music player /// - public static MusicStatus Status => Internal.GetPlaybackStatus(); + public static MusicStatus Status => Internal?.GetPlaybackStatus() ?? MusicStatus.Undefined; - public static void Play() => Internal.Play(); + public static void Play() => Internal?.Play(); - public static void Pause() => Internal.Pause(); + public static void Pause() => Internal?.Pause(); /// /// Have the Steam Music player play the previous song. /// - public static void PlayPrevious() => Internal.PlayPrevious(); + public static void PlayPrevious() => Internal?.PlayPrevious(); /// /// Have the Steam Music player skip to the next song /// - public static void PlayNext() => Internal.PlayNext(); + public static void PlayNext() => Internal?.PlayNext(); /// /// Gets/Sets the current volume of the Steam Music player /// public static float Volume { - get => Internal.GetVolume(); - set => Internal.SetVolume( value ); + get => Internal?.GetVolume() ?? 0f; + set => Internal?.SetVolume( value ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/SteamNetworking.cs index f211214c4..cadac81dd 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworking.cs @@ -10,7 +10,7 @@ namespace Steamworks { public class SteamNetworking : SteamSharedClass { - internal static ISteamNetworking Internal => Interface as ISteamNetworking; + internal static ISteamNetworking? Internal => Interface as ISteamNetworking; internal override void InitializeInterface( bool server ) { @@ -35,26 +35,26 @@ namespace Steamworks /// This SteamId wants to send you a message. You should respond by calling AcceptP2PSessionWithUser /// if you want to recieve their messages /// - public static Action OnP2PSessionRequest; + public static Action? OnP2PSessionRequest; /// /// Called when packets can't get through to the specified user. /// All queued packets unsent at this point will be dropped, further attempts /// to send will retry making the connection (but will be dropped if we fail again). /// - public static Action OnP2PConnectionFailed; + public static Action? OnP2PConnectionFailed; /// /// This should be called in response to a OnP2PSessionRequest /// - public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal.AcceptP2PSessionWithUser( user ); + public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal != null && Internal.AcceptP2PSessionWithUser( user ); /// /// Allow or disallow P2P connects to fall back on Steam server relay if direct /// connection or NAT traversal can't be established. Applies to connections /// created after setting or old connections that need to reconnect. /// - public static bool AllowP2PPacketRelay( bool allow ) => Internal.AllowP2PPacketRelay( allow ); + public static bool AllowP2PPacketRelay( bool allow ) => Internal != null && Internal.AllowP2PPacketRelay( allow ); /// /// This should be called when you're done communicating with a user, as this will @@ -62,7 +62,7 @@ namespace Steamworks /// If the remote user tries to send data to you again, a new OnP2PSessionRequest /// callback will be posted /// - public static bool CloseP2PSessionWithUser( SteamId user ) => Internal.CloseP2PSessionWithUser( user ); + public static bool CloseP2PSessionWithUser( SteamId user ) => Internal != null && Internal.CloseP2PSessionWithUser( user ); /// /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. @@ -70,7 +70,7 @@ namespace Steamworks public static bool IsP2PPacketAvailable( int channel = 0 ) { uint _ = 0; - return Internal.IsP2PPacketAvailable( ref _, channel ); + return Internal != null && Internal.IsP2PPacketAvailable( ref _, channel ); } /// @@ -80,7 +80,7 @@ namespace Steamworks { uint size = 0; - if ( !Internal.IsP2PPacketAvailable( ref size, channel ) ) + if ( Internal is null || !Internal.IsP2PPacketAvailable( ref size, channel ) ) return null; var buffer = Helpers.TakeBuffer( (int) size ); @@ -108,7 +108,7 @@ namespace Steamworks public unsafe static bool ReadP2PPacket( byte[] buffer, ref uint size, ref SteamId steamid, int channel = 0 ) { fixed (byte* p = buffer) { - return Internal.ReadP2PPacket( (IntPtr)p, (uint)buffer.Length, ref size, ref steamid, channel ); + return Internal != null && Internal.ReadP2PPacket( (IntPtr)p, (uint)buffer.Length, ref size, ref steamid, channel ); } } @@ -117,7 +117,7 @@ namespace Steamworks /// public unsafe static bool ReadP2PPacket( byte* buffer, uint cbuf, ref uint size, ref SteamId steamid, int channel = 0 ) { - return Internal.ReadP2PPacket( (IntPtr)buffer, cbuf, ref size, ref steamid, channel ); + return Internal != null && Internal.ReadP2PPacket( (IntPtr)buffer, cbuf, ref size, ref steamid, channel ); } /// @@ -132,7 +132,7 @@ namespace Steamworks fixed ( byte* p = data ) { - return Internal.SendP2PPacket( steamid, (IntPtr)p, (uint)length, (P2PSend)sendType, nChannel ); + return Internal != null && Internal.SendP2PPacket( steamid, (IntPtr)p, (uint)length, (P2PSend)sendType, nChannel ); } } @@ -143,13 +143,13 @@ namespace Steamworks /// public static unsafe bool SendP2PPacket( SteamId steamid, byte* data, uint length, int nChannel = 1, P2PSend sendType = P2PSend.Reliable ) { - return Internal.SendP2PPacket( steamid, (IntPtr)data, (uint)length, (P2PSend)sendType, nChannel ); + return Internal != null && Internal.SendP2PPacket( steamid, (IntPtr)data, (uint)length, (P2PSend)sendType, nChannel ); } public static P2PSessionState? GetP2PSessionState( SteamId steamid ) { P2PSessionState_t state = new P2PSessionState_t(); - if (Internal.GetP2PSessionState(steamid, ref state)) + if (Internal != null && Internal.GetP2PSessionState(steamid, ref state)) { return new P2PSessionState(state); } diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs index ea5e983ac..50e80dfd2 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs @@ -10,7 +10,7 @@ namespace Steamworks { public class SteamNetworkingSockets : SteamSharedClass { - internal static ISteamNetworkingSockets Internal => Interface as ISteamNetworkingSockets; + internal static ISteamNetworkingSockets? Internal => Interface as ISteamNetworkingSockets; internal override void InitializeInterface( bool server ) { @@ -22,7 +22,7 @@ namespace Steamworks static readonly Dictionary SocketInterfaces = new Dictionary(); - internal static SocketManager GetSocketManager( uint id ) + internal static SocketManager? GetSocketManager( uint id ) { if ( SocketInterfaces == null ) return null; if ( id == 0 ) throw new System.ArgumentException( "Invalid Socket" ); @@ -43,7 +43,7 @@ namespace Steamworks #region ConnectionInterface static readonly Dictionary ConnectionInterfaces = new Dictionary(); - internal static ConnectionManager GetConnectionManager( uint id ) + internal static ConnectionManager? GetConnectionManager( uint id ) { if ( ConnectionInterfaces == null ) return null; if ( id == 0 ) return null; @@ -88,7 +88,7 @@ namespace Steamworks OnConnectionStatusChanged?.Invoke( data.Conn, data.Nfo ); } - public static event Action OnConnectionStatusChanged; + public static event Action? OnConnectionStatusChanged; /// @@ -98,8 +98,10 @@ namespace Steamworks /// To use this derive a class from SocketManager and override as much as you want. /// /// - public static T CreateNormalSocket( NetAddress address ) where T : SocketManager, new() + public static T? CreateNormalSocket( NetAddress address ) where T : SocketManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Socket = Internal.CreateListenSocketIP( ref address, options.Length, options ); @@ -118,8 +120,10 @@ namespace Steamworks /// will received all the appropriate callbacks. /// /// - public static SocketManager CreateNormalSocket( NetAddress address, ISocketManager intrface ) + public static SocketManager? CreateNormalSocket( NetAddress address, ISocketManager intrface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var socket = Internal.CreateListenSocketIP( ref address, options.Length, options ); @@ -138,8 +142,10 @@ namespace Steamworks /// /// Connect to a socket created via CreateListenSocketIP /// - public static T ConnectNormal( NetAddress address ) where T : ConnectionManager, new() + public static T? ConnectNormal( NetAddress address ) where T : ConnectionManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Connection = Internal.ConnectByIPAddress( ref address, options.Length, options ); @@ -150,8 +156,10 @@ namespace Steamworks /// /// Connect to a socket created via CreateListenSocketIP /// - public static ConnectionManager ConnectNormal( NetAddress address, IConnectionManager iface ) + public static ConnectionManager? ConnectNormal( NetAddress address, IConnectionManager iface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var connection = Internal.ConnectByIPAddress( ref address, options.Length, options ); @@ -171,8 +179,10 @@ namespace Steamworks /// To use this derive a class from SocketManager and override as much as you want. /// /// - public static T CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() + public static T? CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Socket = Internal.CreateListenSocketP2P( virtualport, options.Length, options ); @@ -189,8 +199,10 @@ namespace Steamworks /// will received all the appropriate callbacks. /// /// - public static SocketManager CreateRelaySocket( int virtualport, ISocketManager intrface ) + public static SocketManager? CreateRelaySocket( int virtualport, ISocketManager intrface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var socket = Internal.CreateListenSocketP2P( virtualport, options.Length, options ); @@ -209,8 +221,10 @@ namespace Steamworks /// /// Connect to a relay server /// - public static T ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() + public static T? ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() { + if (Internal is null) { return null; } + var t = new T(); NetIdentity identity = serverId; var options = Array.Empty(); diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index e78c07bf0..319e5d641 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamNetworkingUtils : SteamSharedClass { - internal static ISteamNetworkingUtils Internal => Interface as ISteamNetworkingUtils; + internal static ISteamNetworkingUtils? Internal => Interface as ISteamNetworkingUtils; internal override void InitializeInterface( bool server ) { @@ -43,7 +43,7 @@ namespace Steamworks /// and your frame rate will tank and you won't know why. /// - public static event Action OnDebugOutput; + public static event Action? OnDebugOutput; public struct SteamRelayNetworkStatus { @@ -82,7 +82,7 @@ namespace Steamworks /// public static void InitRelayNetworkAccess() { - Internal.InitRelayNetworkAccess(); + Internal?.InitRelayNetworkAccess(); } /// @@ -98,6 +98,8 @@ namespace Steamworks { get { + if (Internal is null) { return null; } + NetPingLocation location = default; var age = Internal.GetLocalPingLocation( ref location ); if ( age < 0 ) @@ -114,7 +116,7 @@ namespace Steamworks /// public static int EstimatePingTo( NetPingLocation target ) { - return Internal.EstimatePingTimeFromLocalHost( ref target ); + return Internal?.EstimatePingTimeFromLocalHost( ref target ) ?? 0; } /// @@ -124,7 +126,7 @@ namespace Steamworks public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { await Task.Yield(); - if ( Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) + if ( Internal is null || Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) return; SteamRelayNetworkStatus_t status = default; @@ -135,7 +137,7 @@ namespace Steamworks } } - public static long LocalTimestamp => Internal.GetLocalTimestamp(); + public static long LocalTimestamp => Internal?.GetLocalTimestamp() ?? 0; /// @@ -223,7 +225,7 @@ namespace Steamworks _debugLevel = value; _debugFunc = new NetDebugFunc( OnDebugMessage ); - Internal.SetDebugOutputFunction( value, _debugFunc ); + Internal?.SetDebugOutputFunction( value, _debugFunc ); } } @@ -235,7 +237,7 @@ namespace Steamworks /// /// We need to keep the delegate around until it's not used anymore /// - static NetDebugFunc _debugFunc; + static NetDebugFunc? _debugFunc; struct DebugMessage { @@ -274,7 +276,7 @@ namespace Steamworks internal unsafe static bool SetConfigInt( NetConfig type, int value ) { int* ptr = &value; - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Int32, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Int32, (IntPtr)ptr ); } internal unsafe static int GetConfigInt( NetConfig type ) @@ -283,7 +285,7 @@ namespace Steamworks NetConfigType dtype = NetConfigType.Int32; int* ptr = &value; UIntPtr size = new UIntPtr( sizeof( int ) ); - var result = Internal.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr) ptr, ref size ); + var result = Internal?.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr) ptr, ref size ); if ( result != NetConfigResult.OK ) return 0; @@ -293,7 +295,7 @@ namespace Steamworks internal unsafe static bool SetConfigFloat( NetConfig type, float value ) { float* ptr = &value; - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Float, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Float, (IntPtr)ptr ); } internal unsafe static float GetConfigFloat( NetConfig type ) @@ -302,7 +304,7 @@ namespace Steamworks NetConfigType dtype = NetConfigType.Float; float* ptr = &value; UIntPtr size = new UIntPtr( sizeof( float ) ); - var result = Internal.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr)ptr, ref size ); + var result = Internal?.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr)ptr, ref size ); if ( result != NetConfigResult.OK ) return 0; @@ -315,7 +317,7 @@ namespace Steamworks fixed ( byte* ptr = bytes ) { - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.String, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.String, (IntPtr)ptr ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamParental.cs b/Libraries/Facepunch.Steamworks/SteamParental.cs index b746ca3b6..7f3a23bd6 100644 --- a/Libraries/Facepunch.Steamworks/SteamParental.cs +++ b/Libraries/Facepunch.Steamworks/SteamParental.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamParental : SteamSharedClass { - internal static ISteamParentalSettings Internal => Interface as ISteamParentalSettings; + internal static ISteamParentalSettings? Internal => Interface as ISteamParentalSettings; internal override void InitializeInterface( bool server ) { @@ -28,37 +28,37 @@ namespace Steamworks /// /// Parental Settings Changed /// - public static event Action OnSettingsChanged; + public static event Action? OnSettingsChanged; /// /// /// - public static bool IsParentalLockEnabled => Internal.BIsParentalLockEnabled(); + public static bool IsParentalLockEnabled => Internal != null && Internal.BIsParentalLockEnabled(); /// /// /// - public static bool IsParentalLockLocked => Internal.BIsParentalLockLocked(); + public static bool IsParentalLockLocked => Internal != null && Internal.BIsParentalLockLocked(); /// /// /// - public static bool IsAppBlocked( AppId app ) => Internal.BIsAppBlocked( app.Value ); + public static bool IsAppBlocked( AppId app ) => Internal != null && Internal.BIsAppBlocked( app.Value ); /// /// /// - public static bool BIsAppInBlockList( AppId app ) => Internal.BIsAppInBlockList( app.Value ); + public static bool BIsAppInBlockList( AppId app ) => Internal != null && Internal.BIsAppInBlockList( app.Value ); /// /// /// - public static bool IsFeatureBlocked( ParentalFeature feature ) => Internal.BIsFeatureBlocked( feature ); + public static bool IsFeatureBlocked( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureBlocked( feature ); /// /// /// - public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal.BIsFeatureInBlockList( feature ); + public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureInBlockList( feature ); } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamParties.cs b/Libraries/Facepunch.Steamworks/SteamParties.cs index aef57bb69..1d7c1eba3 100644 --- a/Libraries/Facepunch.Steamworks/SteamParties.cs +++ b/Libraries/Facepunch.Steamworks/SteamParties.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamParties : SteamClientClass { - internal static ISteamParties Internal => Interface as ISteamParties; + internal static ISteamParties? Internal => Interface as ISteamParties; internal override void InitializeInterface( bool server ) { @@ -32,15 +32,15 @@ namespace Steamworks /// /// The list of possible Party beacon locations has changed /// - public static event Action OnBeaconLocationsUpdated; + public static event Action? OnBeaconLocationsUpdated; /// /// The list of active beacons may have changed /// - public static event Action OnActiveBeaconsUpdated; + public static event Action? OnActiveBeaconsUpdated; - public static int ActiveBeaconCount => (int) Internal.GetNumActiveBeacons(); + public static int ActiveBeaconCount => (int)(Internal?.GetNumActiveBeacons() ?? 0); public static IEnumerable ActiveBeacons { @@ -50,7 +50,7 @@ namespace Steamworks { yield return new PartyBeacon { - Id = Internal.GetBeaconByIndex( i ) + Id = Internal?.GetBeaconByIndex( i ) ?? 0 }; } } diff --git a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs index 48a1945d2..502d37291 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamRemotePlay : SteamClientClass { - internal static ISteamRemotePlay Internal => Interface as ISteamRemotePlay; + internal static ISteamRemotePlay? Internal => Interface as ISteamRemotePlay; internal override void InitializeInterface( bool server ) { @@ -30,29 +30,29 @@ namespace Steamworks /// /// Called when a session is connected /// - public static event Action OnSessionConnected; + public static event Action? OnSessionConnected; /// /// Called when a session becomes disconnected /// - public static event Action OnSessionDisconnected; + public static event Action? OnSessionDisconnected; /// /// Get the number of currently connected Steam Remote Play sessions /// - public static int SessionCount => (int) Internal.GetSessionCount(); + public static int SessionCount => (int)(Internal?.GetSessionCount() ?? 0); /// /// Get the currently connected Steam Remote Play session ID at the specified index. /// IsValid will return false if it's out of bounds /// - public static RemotePlaySession GetSession( int index ) => (RemotePlaySession) Internal.GetSessionID( index ).Value; + public static RemotePlaySession GetSession( int index ) => Internal?.GetSessionID( index ).Value ?? default; /// /// Invite a friend to Remote Play Together /// This returns false if the invite can't be sent /// - public static bool SendInvite( SteamId steamid ) => Internal.BSendRemotePlayTogetherInvite( steamid ); + public static bool SendInvite( SteamId steamid ) => Internal != null && Internal.BSendRemotePlayTogetherInvite( steamid ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs index bfb9a0a6b..847147680 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamRemoteStorage : SteamClientClass { - internal static ISteamRemoteStorage Internal => Interface as ISteamRemoteStorage; + internal static ISteamRemoteStorage? Internal => Interface as ISteamRemoteStorage; internal override void InitializeInterface( bool server ) { @@ -28,15 +28,17 @@ namespace Steamworks { fixed ( byte* ptr = data ) { - return Internal.FileWrite( filename, (IntPtr) ptr, data.Length ); + return Internal != null && Internal.FileWrite( filename, (IntPtr) ptr, data.Length ); } } /// /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. /// - public unsafe static byte[] FileRead( string filename ) + public unsafe static byte[]? FileRead( string filename ) { + if (Internal is null) { return null; } + var size = FileSize( filename ); if ( size <= 0 ) return null; var buffer = new byte[size]; @@ -51,32 +53,32 @@ namespace Steamworks /// /// Checks whether the specified file exists. /// - public static bool FileExists( string filename ) => Internal.FileExists( filename ); + public static bool FileExists( string filename ) => Internal != null && Internal.FileExists( filename ); /// /// Checks if a specific file is persisted in the steam cloud. /// - public static bool FilePersisted( string filename ) => Internal.FilePersisted( filename ); + public static bool FilePersisted( string filename ) => Internal != null && Internal.FilePersisted( filename ); /// /// Gets the specified file's last modified date/time. /// - public static DateTime FileTime( string filename ) => Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ); + public static DateTime FileTime( string filename ) => Internal != null ? Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ) : default; /// /// Gets the specified files size in bytes. 0 if not exists. /// - public static int FileSize( string filename ) => Internal.GetFileSize( filename ); + public static int FileSize( string filename ) => Internal?.GetFileSize( filename ) ?? 0; /// /// Deletes the file from remote storage, but leaves it on the local disk and remains accessible from the API. /// - public static bool FileForget( string filename ) => Internal.FileForget( filename ); + public static bool FileForget( string filename ) => Internal != null && Internal.FileForget( filename ); /// /// Deletes a file from the local disk, and propagates that delete to the cloud. /// - public static bool FileDelete( string filename ) => Internal.FileDelete( filename ); + public static bool FileDelete( string filename ) => Internal != null && Internal.FileDelete( filename ); /// @@ -87,7 +89,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return t; } } @@ -100,7 +102,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return t - a; } } @@ -113,7 +115,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return a; } } @@ -127,7 +129,7 @@ namespace Steamworks /// Checks if the account wide Steam Cloud setting is enabled for this user /// or if they disabled it in the Settings->Cloud dialog. /// - public static bool IsCloudEnabledForAccount => Internal.IsCloudEnabledForAccount(); + public static bool IsCloudEnabledForAccount => Internal != null && Internal.IsCloudEnabledForAccount(); /// /// Checks if the per game Steam Cloud setting is enabled for this user @@ -139,14 +141,14 @@ namespace Steamworks /// public static bool IsCloudEnabledForApp { - get => Internal.IsCloudEnabledForApp(); - set => Internal.SetCloudEnabledForApp( value ); + get => Internal != null && Internal.IsCloudEnabledForApp(); + set => Internal?.SetCloudEnabledForApp( value ); } /// /// Gets the total number of local files synchronized by Steam Cloud. /// - public static int FileCount => Internal.GetFileCount(); + public static int FileCount => Internal?.GetFileCount() ?? 0; public struct RemoteFile { @@ -155,7 +157,7 @@ namespace Steamworks public bool Delete() { - return Internal.FileDelete(Filename); + return Internal != null && Internal.FileDelete(Filename); } } @@ -167,6 +169,8 @@ namespace Steamworks get { var ret = new List(); + if (Internal is null) { return ret; } + int count = FileCount; for( int i=0; i public class SteamScreenshots : SteamClientClass { - internal static ISteamScreenshots Internal => Interface as ISteamScreenshots; + internal static ISteamScreenshots? Internal => Interface as ISteamScreenshots; internal override void InitializeInterface( bool server ) { @@ -37,17 +37,17 @@ namespace Steamworks /// This will only be called if Hooked is true, in which case Steam /// will not take the screenshot itself. /// - public static event Action OnScreenshotRequested; + public static event Action? OnScreenshotRequested; /// /// A screenshot successfully written or otherwise added to the library and can now be tagged. /// - public static event Action OnScreenshotReady; + public static event Action? OnScreenshotReady; /// /// A screenshot attempt failed /// - public static event Action OnScreenshotFailed; + public static event Action? OnScreenshotFailed; /// /// Writes a screenshot to the user's screenshot library given the raw image data, which must be in RGB format. @@ -55,6 +55,8 @@ namespace Steamworks /// public unsafe static Screenshot? WriteScreenshot( byte[] data, int width, int height ) { + if (Internal is null) { return null; } + fixed ( byte* ptr = data ) { var handle = Internal.WriteScreenshot( (IntPtr)ptr, (uint)data.Length, width, height ); @@ -72,6 +74,8 @@ namespace Steamworks /// public unsafe static Screenshot? AddScreenshot( string filename, string thumbnail, int width, int height ) { + if (Internal is null) { return null; } + var handle = Internal.AddScreenshotToLibrary( filename, thumbnail, width, height ); if ( handle.Value == 0 ) return null; @@ -83,7 +87,7 @@ namespace Steamworks /// If screenshots are being hooked by the game then a /// ScreenshotRequested callback is sent back to the game instead. /// - public static void TriggerScreenshot() => Internal.TriggerScreenshot(); + public static void TriggerScreenshot() => Internal?.TriggerScreenshot(); /// /// Toggles whether the overlay handles screenshots when the user presses the screenshot hotkey, or if the game handles them. @@ -93,8 +97,8 @@ namespace Steamworks /// public static bool Hooked { - get => Internal.IsScreenshotsHooked(); - set => Internal.HookScreenshots( value ); + get => Internal != null && Internal.IsScreenshotsHooked(); + set => Internal?.HookScreenshots( value ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index 1b1559d45..b3ea1bef5 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Steamworks.Data; +using System.Diagnostics.CodeAnalysis; namespace Steamworks { @@ -12,7 +13,7 @@ namespace Steamworks /// public partial class SteamServer : SteamServerClass { - internal static ISteamGameServer Internal => Interface as ISteamGameServer; + internal static ISteamGameServer? Internal => Interface as ISteamGameServer; internal override void InitializeInterface( bool server ) { @@ -34,28 +35,28 @@ namespace Steamworks /// /// User has been authed or rejected /// - public static event Action OnValidateAuthTicketResponse; + public static event Action? OnValidateAuthTicketResponse; /// /// Called when a connections to the Steam back-end has been established. /// This means the server now is logged on and has a working connection to the Steam master server. /// - public static event Action OnSteamServersConnected; + public static event Action? OnSteamServersConnected; /// /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying) /// - public static event Action OnSteamServerConnectFailure; + public static event Action? OnSteamServerConnectFailure; /// /// Disconnected from Steam /// - public static event Action OnSteamServersDisconnected; + public static event Action? OnSteamServersDisconnected; /// /// Called when authentication status changes, useful for grabbing SteamId once aavailability is current /// - public static event Action OnSteamNetAuthenticationStatus; + public static event Action? OnSteamNetAuthenticationStatus; /// @@ -172,7 +173,7 @@ namespace Steamworks public static bool DedicatedServer { get => _dedicatedServer; - set { if ( _dedicatedServer == value ) return; Internal.SetDedicatedServer( value ); _dedicatedServer = value; } + set { if ( _dedicatedServer == value ) return; Internal?.SetDedicatedServer( value ); _dedicatedServer = value; } } private static bool _dedicatedServer; @@ -183,7 +184,7 @@ namespace Steamworks public static int MaxPlayers { get => _maxplayers; - set { if ( _maxplayers == value ) return; Internal.SetMaxPlayerCount( value ); _maxplayers = value; } + set { if ( _maxplayers == value ) return; Internal?.SetMaxPlayerCount( value ); _maxplayers = value; } } private static int _maxplayers = 0; @@ -194,7 +195,7 @@ namespace Steamworks public static int BotCount { get => _botcount; - set { if ( _botcount == value ) return; Internal.SetBotPlayerCount( value ); _botcount = value; } + set { if ( _botcount == value ) return; Internal?.SetBotPlayerCount( value ); _botcount = value; } } private static int _botcount = 0; @@ -204,9 +205,9 @@ namespace Steamworks public static string MapName { get => _mapname; - set { if ( _mapname == value ) return; Internal.SetMapName( value ); _mapname = value; } + set { if ( _mapname == value ) return; Internal?.SetMapName( value ); _mapname = value; } } - private static string _mapname; + private static string _mapname = ""; /// /// Gets or sets the current ModDir @@ -214,7 +215,7 @@ namespace Steamworks public static string ModDir { get => _modDir; - internal set { if ( _modDir == value ) return; Internal.SetModDir( value ); _modDir = value; } + internal set { if ( _modDir == value ) return; Internal?.SetModDir( value ); _modDir = value; } } private static string _modDir = ""; @@ -224,7 +225,7 @@ namespace Steamworks public static string Product { get => _product; - internal set { if ( _product == value ) return; Internal.SetProduct( value ); _product = value; } + internal set { if ( _product == value ) return; Internal?.SetProduct( value ); _product = value; } } private static string _product = ""; @@ -234,7 +235,7 @@ namespace Steamworks public static string GameDescription { get => _gameDescription; - internal set { if ( _gameDescription == value ) return; Internal.SetGameDescription( value ); _gameDescription = value; } + internal set { if ( _gameDescription == value ) return; Internal?.SetGameDescription( value ); _gameDescription = value; } } private static string _gameDescription = ""; @@ -244,7 +245,7 @@ namespace Steamworks public static string ServerName { get => _serverName; - set { if ( _serverName == value ) return; Internal.SetServerName( value ); _serverName = value; } + set { if ( _serverName == value ) return; Internal?.SetServerName( value ); _serverName = value; } } private static string _serverName = ""; @@ -254,7 +255,7 @@ namespace Steamworks public static bool Passworded { get => _passworded; - set { if ( _passworded == value ) return; Internal.SetPasswordProtected( value ); _passworded = value; } + set { if ( _passworded == value ) return; Internal?.SetPasswordProtected( value ); _passworded = value; } } private static bool _passworded; @@ -268,20 +269,20 @@ namespace Steamworks set { if ( _gametags == value ) return; - Internal.SetGameTags( value ); + Internal?.SetGameTags( value ); _gametags = value; } } private static string _gametags = ""; - public static SteamId SteamId => Internal.GetSteamID(); + public static SteamId SteamId => Internal?.GetSteamID() ?? default; /// /// Log onto Steam anonymously. /// public static void LogOnAnonymous() { - Internal.LogOnAnonymous(); + Internal?.LogOnAnonymous(); ForceHeartbeat(); } @@ -290,21 +291,21 @@ namespace Steamworks /// public static void LogOff() { - Internal.LogOff(); + Internal?.LogOff(); } /// /// Returns true if the server is connected and registered with the Steam master server /// You should have called LogOnAnonymous etc on startup. /// - public static bool LoggedOn => Internal.BLoggedOn(); + public static bool LoggedOn => Internal != null && Internal.BLoggedOn(); /// /// To the best of its ability this tries to get the server's /// current public ip address. Be aware that this is likely to return /// null for the first few seconds after initialization. /// - public static System.Net.IPAddress PublicIp => Internal.GetPublicIP(); + public static System.Net.IPAddress? PublicIp => Internal?.GetPublicIP(); /// /// Enable or disable heartbeats, which are sent regularly to the master server. @@ -312,7 +313,7 @@ namespace Steamworks /// public static bool AutomaticHeartbeats { - set { Internal.EnableHeartbeats( value ); } + set { Internal?.EnableHeartbeats( value ); } } /// @@ -321,7 +322,7 @@ namespace Steamworks /// public static int AutomaticHeartbeatRate { - set { Internal.SetHeartbeatInterval( value ); } + set { Internal?.SetHeartbeatInterval( value ); } } /// @@ -330,7 +331,7 @@ namespace Steamworks /// public static void ForceHeartbeat() { - Internal.ForceHeartbeat(); + Internal?.ForceHeartbeat(); } /// @@ -340,7 +341,7 @@ namespace Steamworks /// public static void UpdatePlayer( SteamId steamid, string name, int score ) { - Internal.BUpdateUserData( steamid, name, (uint)score ); + Internal?.BUpdateUserData( steamid, name, (uint)score ); } static Dictionary KeyValue = new Dictionary(); @@ -353,6 +354,8 @@ namespace Steamworks /// public static void SetKey( string Key, string Value ) { + if (Internal is null) { return; } + if ( KeyValue.ContainsKey( Key ) ) { if ( KeyValue[Key] == Value ) @@ -374,7 +377,7 @@ namespace Steamworks public static void ClearKeys() { KeyValue.Clear(); - Internal.ClearAllKeyValues(); + Internal?.ClearAllKeyValues(); } /// @@ -382,6 +385,7 @@ namespace Steamworks /// public static unsafe BeginAuthResult BeginAuthSession( byte[] data, SteamId steamid ) { + if (Internal is null) { return BeginAuthResult.ServerNotConnectedToSteam; } fixed ( byte* p = data ) { var result = Internal.BeginAuthSession( (IntPtr)p, data.Length, steamid ); @@ -395,7 +399,7 @@ namespace Steamworks /// public static void EndSession( SteamId steamid ) { - Internal.EndAuthSession( steamid ); + Internal?.EndAuthSession( steamid ); } /// @@ -406,6 +410,12 @@ namespace Steamworks /// True if we want to send a packet public static unsafe bool GetOutgoingPacket( out OutgoingPacket packet ) { + if (Internal is null) + { + packet = default; + return false; + } + var buffer = Helpers.TakeBuffer( 1024 * 32 ); packet = new OutgoingPacket(); @@ -442,7 +452,7 @@ namespace Steamworks /// public static unsafe void HandleIncomingPacket( IntPtr ptr, int size, uint address, ushort port ) { - Internal.HandleIncomingPacket( ptr, size, address, port ); + Internal?.HandleIncomingPacket( ptr, size, address, port ); } /// @@ -450,7 +460,7 @@ namespace Steamworks /// public static UserHasLicenseForAppResult UserHasLicenseForApp( SteamId steamid, AppId appid ) { - return Internal.UserHasLicenseForApp( steamid, appid ); + return Internal?.UserHasLicenseForApp( steamid, appid ) ?? UserHasLicenseForAppResult.NoAuth; } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServerStats.cs b/Libraries/Facepunch.Steamworks/SteamServerStats.cs index 2de7b0f14..823a92771 100644 --- a/Libraries/Facepunch.Steamworks/SteamServerStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamServerStats.cs @@ -9,7 +9,7 @@ namespace Steamworks { public class SteamServerStats : SteamServerClass { - internal static ISteamGameServerStats Internal => Interface as ISteamGameServerStats; + internal static ISteamGameServerStats? Internal => Interface as ISteamGameServerStats; internal override void InitializeInterface( bool server ) { @@ -24,6 +24,7 @@ namespace Steamworks /// public static async Task RequestUserStatsAsync( SteamId steamid ) { + if (Internal is null) { return Result.Fail; } var r = await Internal.RequestUserStats( steamid ); if ( !r.HasValue ) return Result.Fail; return r.Value.Result; @@ -35,7 +36,7 @@ namespace Steamworks /// public static bool SetInt( SteamId steamid, string name, int stat ) { - return Internal.SetUserStat( steamid, name, stat ); + return Internal != null && Internal.SetUserStat( steamid, name, stat ); } /// @@ -44,7 +45,7 @@ namespace Steamworks /// public static bool SetFloat( SteamId steamid, string name, float stat ) { - return Internal.SetUserStat( steamid, name, stat ); + return Internal != null && Internal.SetUserStat( steamid, name, stat ); } /// @@ -56,7 +57,7 @@ namespace Steamworks { int data = defaultValue; - if ( !Internal.GetUserStat( steamid, name, ref data ) ) + if ( Internal is null || !Internal.GetUserStat( steamid, name, ref data ) ) return defaultValue; return data; @@ -71,7 +72,7 @@ namespace Steamworks { float data = defaultValue; - if ( !Internal.GetUserStat( steamid, name, ref data ) ) + if ( Internal is null || !Internal.GetUserStat( steamid, name, ref data ) ) return defaultValue; return data; @@ -83,7 +84,7 @@ namespace Steamworks /// public static bool SetAchievement( SteamId steamid, string name ) { - return Internal.SetUserAchievement( steamid, name ); + return Internal != null && Internal.SetUserAchievement( steamid, name ); } /// @@ -92,7 +93,7 @@ namespace Steamworks /// public static bool ClearAchievement( SteamId steamid, string name ) { - return Internal.ClearUserAchievement( steamid, name ); + return Internal != null && Internal.ClearUserAchievement( steamid, name ); } /// @@ -102,7 +103,7 @@ namespace Steamworks { bool achieved = false; - if ( !Internal.GetUserAchievement( steamid, name, ref achieved ) ) + if ( Internal is null || !Internal.GetUserAchievement( steamid, name, ref achieved ) ) return false; return achieved; @@ -115,6 +116,7 @@ namespace Steamworks /// public static async Task StoreUserStats( SteamId steamid ) { + if (Internal is null) { return Result.Fail; } var r = await Internal.StoreUserStats( steamid ); if ( !r.HasValue ) return Result.Fail; return r.Value.Result; diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 21c51a991..ecf20286e 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamUGC : SteamSharedClass { - internal static ISteamUGC Internal => Interface as ISteamUGC; + internal static ISteamUGC? Internal => Interface as ISteamUGC; internal override void InitializeInterface( bool server ) { @@ -44,10 +44,11 @@ namespace Steamworks /// /// Posted after Download call /// - public static event Action OnDownloadItemResult; + public static event Action? OnDownloadItemResult; public static async Task DeleteFileAsync( PublishedFileId fileId ) { + if (Internal is null) { return false; } var r = await Internal.DeleteItem( fileId ); return r?.Result == Result.OK; } @@ -60,7 +61,7 @@ namespace Steamworks /// true if nothing went wrong and the download is started public static bool Download( PublishedFileId fileId, bool highPriority = false ) { - return Internal.DownloadItem( fileId, highPriority ); + return Internal != null && Internal.DownloadItem( fileId, highPriority ); } /// @@ -73,7 +74,7 @@ namespace Steamworks /// true if downloaded and installed correctly public static async Task DownloadAsync( PublishedFileId fileId, - Action progress = null, + Action? progress = null, int millisecondsUpdateDelay = 60, CancellationToken? ct = null) { @@ -163,28 +164,32 @@ namespace Steamworks public static async Task StartPlaytimeTracking(PublishedFileId fileId) { + if (Internal is null) { return false; } var result = await Internal.StartPlaytimeTracking(new[] {fileId}, 1); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } public static async Task StopPlaytimeTracking(PublishedFileId fileId) { + if (Internal is null) { return false; } var result = await Internal.StopPlaytimeTracking(new[] {fileId}, 1); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } public static async Task StopPlaytimeTrackingForAllItems() { + if (Internal is null) { return false; } var result = await Internal.StopPlaytimeTrackingForAllItems(); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } - public static Action GlobalOnItemInstalled; + public static Action? GlobalOnItemInstalled; - public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } + public static uint NumSubscribedItems { get { return Internal?.GetNumSubscribedItems() ?? 0; } } public static PublishedFileId[] GetSubscribedItems() { + if (Internal is null) { return Array.Empty(); } uint numSubscribed = NumSubscribedItems; PublishedFileId[] ids = new PublishedFileId[numSubscribed]; Internal.GetSubscribedItems(ids, numSubscribed); diff --git a/Libraries/Facepunch.Steamworks/SteamUser.cs b/Libraries/Facepunch.Steamworks/SteamUser.cs index e481d8493..8f4c6620e 100644 --- a/Libraries/Facepunch.Steamworks/SteamUser.cs +++ b/Libraries/Facepunch.Steamworks/SteamUser.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamUser : SteamClientClass { - internal static ISteamUser Internal => Interface as ISteamUser; + internal static ISteamUser? Internal => Interface as ISteamUser; internal override void InitializeInterface( bool server ) { @@ -26,7 +26,7 @@ namespace Steamworks SampleRate = OptimalSampleRate; } - static Dictionary richPresence; + static Dictionary? richPresence; internal static void InstallEvents() { @@ -48,20 +48,20 @@ namespace Steamworks /// Usually this will have occurred before the game has launched, and should only be seen if the /// user has dropped connection due to a networking issue or a Steam server update. /// - public static event Action OnSteamServersConnected; + public static event Action? OnSteamServersConnected; /// /// Called when a connection attempt has failed. /// This will occur periodically if the Steam client is not connected, /// and has failed when retrying to establish a connection. /// - public static event Action OnSteamServerConnectFailure; + public static event Action? OnSteamServerConnectFailure; /// /// Called if the client has lost connection to the Steam servers. /// Real-time services will be disabled until a matching OnSteamServersConnected has been posted. /// - public static event Action OnSteamServersDisconnected; + public static event Action? OnSteamServersDisconnected; /// /// Sent by the Steam server to the client telling it to disconnect from the specified game server, @@ -69,12 +69,12 @@ namespace Steamworks /// The game client should immediately disconnect upon receiving this message. /// This can usually occur if the user doesn't have rights to play on the game server. /// - public static event Action OnClientGameServerDeny; + public static event Action? OnClientGameServerDeny; /// /// Called whenever the users licenses (owned packages) changes. /// - public static event Action OnLicensesUpdated; + public static event Action? OnLicensesUpdated; /// /// Called when an auth ticket has been validated. @@ -82,18 +82,18 @@ namespace Steamworks /// The second is the Steam ID that owns the game, this will be different from the first /// if the game is being borrowed via Steam Family Sharing /// - public static event Action OnValidateAuthTicketResponse; + public static event Action? OnValidateAuthTicketResponse; /// /// Used internally for GetAuthSessionTicketAsync /// - internal static event Action OnGetAuthSessionTicketResponse; + internal static event Action? OnGetAuthSessionTicketResponse; /// /// Called when a user has responded to a microtransaction authorization request. /// ( appid, orderid, user authorized ) /// - public static event Action OnMicroTxnAuthorizationResponse; + public static event Action? OnMicroTxnAuthorizationResponse; /// /// Sent to your game in response to a steam://gamewebcallback/(appid)/command/stuff command from a user clicking a @@ -101,14 +101,14 @@ namespace Steamworks /// You can use this to add support for external site signups where you want to pop back into the browser after some web page /// signup sequence, and optionally get back some detail about that. /// - public static event Action OnGameWebCallback; + public static event Action? OnGameWebCallback; /// /// Sent for games with enabled anti indulgence / duration control, for enabled users. /// Lets the game know whether persistent rewards or XP should be granted at normal rate, /// half rate, or zero rate. /// - public static event Action OnDurationControl; + public static event Action? OnDurationControl; @@ -127,8 +127,8 @@ namespace Steamworks set { _recordingVoice = value; - if ( value ) Internal.StartVoiceRecording(); - else Internal.StopVoiceRecording(); + if ( value ) Internal?.StartVoiceRecording(); + else Internal?.StopVoiceRecording(); } } @@ -142,7 +142,7 @@ namespace Steamworks { uint szCompressed = 0, deprecated = 0; - if ( Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) return false; return szCompressed > 0; @@ -168,7 +168,7 @@ namespace Steamworks fixed ( byte* b = readBuffer ) { - if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return 0; } @@ -185,7 +185,7 @@ namespace Steamworks /// ReadVoiceData because it won't be creating a new byte array every call. But this /// makes it easier to get it working, so let the babies have their bottle. /// - public static unsafe byte[] ReadVoiceDataBytes() + public static unsafe byte[]? ReadVoiceDataBytes() { if ( !HasVoiceData ) return null; @@ -195,7 +195,7 @@ namespace Steamworks fixed ( byte* b = readBuffer ) { - if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return null; } @@ -222,7 +222,7 @@ namespace Steamworks } } - public static uint OptimalSampleRate => Internal.GetVoiceOptimalSampleRate(); + public static uint OptimalSampleRate => Internal?.GetVoiceOptimalSampleRate() ?? 0; /// @@ -247,7 +247,7 @@ namespace Steamworks fixed ( byte* frm = from ) fixed ( byte* dst = to ) { - if ( Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } @@ -273,7 +273,7 @@ namespace Steamworks fixed ( byte* frm = from ) fixed ( byte* dst = to ) { - if ( Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } @@ -297,7 +297,7 @@ namespace Steamworks uint szWritten = 0; - if ( Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; return (int)szWritten; @@ -306,14 +306,14 @@ namespace Steamworks /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// - public static unsafe AuthTicket GetAuthSessionTicket() + public static unsafe AuthTicket? GetAuthSessionTicket() { var data = Helpers.TakeBuffer( 1024 ); fixed ( byte* b = data ) { uint ticketLength = 0; - uint ticket = Internal.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ); + uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ) ?? 0; if ( ticket == 0 ) return null; @@ -332,15 +332,15 @@ namespace Steamworks /// the ticket is definitely ready to go as soon as it returns. Will return null if the callback /// times out or returns negatively. /// - public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) + public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) { var result = Result.Pending; - AuthTicket ticket = null; + AuthTicket? ticket = null; var stopwatch = Stopwatch.StartNew(); void f( GetAuthSessionTicketResponse_t t ) { - if ( t.AuthTicket != ticket.Handle ) return; + if ( t.AuthTicket != ticket?.Handle ) return; result = t.Result; } @@ -379,11 +379,11 @@ namespace Steamworks { fixed ( byte* ptr = ticketData ) { - return Internal.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ); + return Internal?.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ) ?? BeginAuthResult.ServerNotConnectedToSteam; } } - public static void EndAuthSession( SteamId steamid ) => Internal.EndAuthSession( steamid ); + public static void EndAuthSession( SteamId steamid ) => Internal?.EndAuthSession( steamid ); // UserHasLicenseForApp - SERVER VERSION ( DLC CHECKING ) @@ -392,12 +392,12 @@ namespace Steamworks /// Checks if the current users looks like they are behind a NAT device. /// This is only valid if the user is connected to the Steam servers and may not catch all forms of NAT. /// - public static bool IsBehindNAT => Internal.BIsBehindNAT(); + public static bool IsBehindNAT => Internal != null && Internal.BIsBehindNAT(); /// /// Gets the Steam level of the user, as shown on their Steam community profile. /// - public static int SteamLevel => Internal.GetPlayerSteamLevel(); + public static int SteamLevel => Internal?.GetPlayerSteamLevel() ?? 0; /// /// Requests a URL which authenticates an in-game browser for store check-out, and then redirects to the specified URL. @@ -405,8 +405,10 @@ namespace Steamworks /// NOTE: The URL has a very short lifetime to prevent history-snooping attacks, so you should only call this API when you are about to launch the browser, or else immediately navigate to the result URL using a hidden browser window. /// NOTE: The resulting authorization cookie has an expiration time of one day, so it would be a good idea to request and visit a new auth URL every 12 hours. /// - public static async Task GetStoreAuthUrlAsync( string url ) + public static async Task GetStoreAuthUrlAsync( string url ) { + if (Internal is null) { return null; } + var response = await Internal.RequestStoreAuthURL( url ); if ( !response.HasValue ) return null; @@ -417,22 +419,22 @@ namespace Steamworks /// /// Checks whether the current user has verified their phone number. /// - public static bool IsPhoneVerified => Internal.BIsPhoneVerified(); + public static bool IsPhoneVerified => Internal != null && Internal.BIsPhoneVerified(); /// /// Checks whether the current user has Steam Guard two factor authentication enabled on their account. /// - public static bool IsTwoFactorEnabled => Internal.BIsTwoFactorEnabled(); + public static bool IsTwoFactorEnabled => Internal != null && Internal.BIsTwoFactorEnabled(); /// /// Checks whether the user's phone number is used to uniquely identify them. /// - public static bool IsPhoneIdentifying => Internal.BIsPhoneIdentifying(); + public static bool IsPhoneIdentifying => Internal != null && Internal.BIsPhoneIdentifying(); /// /// Checks whether the current user's phone number is awaiting (re)verification. /// - public static bool IsPhoneRequiringVerification => Internal.BIsPhoneRequiringVerification(); + public static bool IsPhoneRequiringVerification => Internal != null && Internal.BIsPhoneRequiringVerification(); /// /// Requests an application ticket encrypted with the secret "encrypted app ticket key". @@ -441,8 +443,10 @@ namespace Steamworks /// If you get a null result from this it's probably because you're calling it too often. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// - public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) + public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) { + if (Internal is null) { return null; } + var dataPtr = Marshal.AllocHGlobal( dataToInclude.Length ); Marshal.Copy( dataToInclude, 0, dataPtr, dataToInclude.Length ); @@ -453,7 +457,7 @@ namespace Steamworks var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; - byte[] data = null; + byte[]? data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { @@ -477,14 +481,16 @@ namespace Steamworks /// There can only be one call pending, and this call is subject to a 60 second rate limit. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// - public static async Task RequestEncryptedAppTicketAsync() + public static async Task RequestEncryptedAppTicketAsync() { + if (Internal is null) { return null; } + var result = await Internal.RequestEncryptedAppTicket( IntPtr.Zero, 0 ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; - byte[] data = null; + byte[]? data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { @@ -504,6 +510,8 @@ namespace Steamworks /// public static async Task GetDurationControl() { + if (Internal is null) { return default; } + var response = await Internal.GetDurationControl(); if ( !response.HasValue ) return default; diff --git a/Libraries/Facepunch.Steamworks/SteamUserStats.cs b/Libraries/Facepunch.Steamworks/SteamUserStats.cs index 97bbb31f0..f9fc6fa5d 100644 --- a/Libraries/Facepunch.Steamworks/SteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamUserStats.cs @@ -9,7 +9,7 @@ namespace Steamworks { public class SteamUserStats : SteamClientClass { - internal static ISteamUserStats Internal => Interface as ISteamUserStats; + internal static ISteamUserStats? Internal => Interface as ISteamUserStats; internal override void InitializeInterface( bool server ) { @@ -40,31 +40,31 @@ namespace Steamworks /// /// called when the achivement icon is loaded /// - internal static event Action OnAchievementIconFetched; + internal static event Action? OnAchievementIconFetched; /// /// called when the latests stats and achievements have been received /// from the server /// - public static event Action OnUserStatsReceived; + public static event Action? OnUserStatsReceived; /// /// result of a request to store the user stats for a game /// - public static event Action OnUserStatsStored; + public static event Action? OnUserStatsStored; /// /// result of a request to store the achievements for a game, or an /// "indicate progress" call. If both m_nCurProgress and m_nMaxProgress /// are zero, that means the achievement has been fully unlocked /// - public static event Action OnAchievementProgress; + public static event Action? OnAchievementProgress; /// /// Callback indicating that a user's stats have been unloaded /// - public static event Action OnUserStatsUnloaded; + public static event Action? OnUserStatsUnloaded; /// /// Get the available achievements @@ -73,8 +73,11 @@ namespace Steamworks { get { - for( int i=0; i< Internal.GetNumAchievements(); i++ ) + if (Internal is null) { yield break; } + uint numAchievements = Internal.GetNumAchievements(); + for( int i=0; i < numAchievements; i++ ) { + if (Internal is null) { yield break; } yield return new Achievement( Internal.GetAchievementName( (uint) i ) ); } } @@ -88,6 +91,8 @@ namespace Steamworks /// public static bool IndicateAchievementProgress( string achName, int curProg, int maxProg ) { + if (Internal is null) { return false; } + if ( string.IsNullOrEmpty( achName ) ) throw new ArgumentNullException( "Achievement string is null or empty" ); @@ -103,6 +108,8 @@ namespace Steamworks /// public static async Task PlayerCountAsync() { + if (Internal is null) { return -1; } + var result = await Internal.GetNumberOfCurrentPlayers(); if ( !result.HasValue || result.Value.Success == 0 ) return -1; @@ -122,7 +129,7 @@ namespace Steamworks /// public static bool StoreStats() { - return Internal.StoreStats(); + return Internal != null && Internal.StoreStats(); } /// @@ -133,7 +140,7 @@ namespace Steamworks /// public static bool RequestCurrentStats() { - return Internal.RequestCurrentStats(); + return Internal != null && Internal.RequestCurrentStats(); } /// @@ -146,6 +153,7 @@ namespace Steamworks /// OK indicates success, InvalidState means you need to call RequestCurrentStats first, Fail means the remote call failed public static async Task RequestGlobalStatsAsync( int days ) { + if (Internal is null) { return Result.Fail; } var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( !result.HasValue ) return Result.Fail; return result.Value.Result; @@ -162,6 +170,7 @@ namespace Steamworks /// public static async Task FindOrCreateLeaderboardAsync( string name, LeaderboardSort sort, LeaderboardDisplay display ) { + if (Internal is null) { return null; } var result = await Internal.FindOrCreateLeaderboard( name, sort, display ); if ( !result.HasValue || result.Value.LeaderboardFound == 0 ) return null; @@ -172,6 +181,7 @@ namespace Steamworks public static async Task FindLeaderboardAsync( string name ) { + if (Internal is null) { return null; } var result = await Internal.FindLeaderboard( name ); if ( !result.HasValue || result.Value.LeaderboardFound == 0 ) return null; @@ -211,7 +221,7 @@ namespace Steamworks /// public static bool SetStat( string name, int value ) { - return Internal.SetStat( name, value ); + return Internal != null && Internal.SetStat( name, value ); } /// @@ -220,7 +230,7 @@ namespace Steamworks /// public static bool SetStat( string name, float value ) { - return Internal.SetStat( name, value ); + return Internal != null && Internal.SetStat( name, value ); } /// @@ -229,7 +239,7 @@ namespace Steamworks public static int GetStatInt( string name ) { int data = 0; - Internal.GetStat( name, ref data ); + Internal?.GetStat( name, ref data ); return data; } @@ -239,7 +249,7 @@ namespace Steamworks public static float GetStatFloat( string name ) { float data = 0; - Internal.GetStat( name, ref data ); + Internal?.GetStat( name, ref data ); return data; } @@ -250,7 +260,7 @@ namespace Steamworks /// public static bool ResetAll( bool includeAchievements ) { - return Internal.ResetAllStats( includeAchievements ); + return Internal != null && Internal.ResetAllStats( includeAchievements ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamUtils.cs b/Libraries/Facepunch.Steamworks/SteamUtils.cs index 9d3eea0d5..3f1553c23 100644 --- a/Libraries/Facepunch.Steamworks/SteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamUtils.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamUtils : SteamSharedClass { - internal static ISteamUtils Internal => Interface as ISteamUtils; + internal static ISteamUtils? Internal => Interface as ISteamUtils; internal override void InitializeInterface( bool server ) { @@ -38,47 +38,47 @@ namespace Steamworks /// /// The country of the user changed /// - public static event Action OnIpCountryChanged; + public static event Action? OnIpCountryChanged; /// /// Fired when running on a laptop and less than 10 minutes of battery is left, fires then every minute /// The parameter is the number of minutes left /// - public static event Action OnLowBatteryPower; + public static event Action? OnLowBatteryPower; /// /// Called when Steam wants to shutdown /// - public static event Action OnSteamShutdown; + public static event Action? OnSteamShutdown; /// /// Big Picture gamepad text input has been closed. Parameter is true if text was submitted, false if cancelled etc. /// - public static event Action OnGamepadTextInputDismissed; + public static event Action? OnGamepadTextInputDismissed; /// /// Returns the number of seconds since the application was active /// - public static uint SecondsSinceAppActive => Internal.GetSecondsSinceAppActive(); + public static uint SecondsSinceAppActive => Internal?.GetSecondsSinceAppActive() ?? 0; /// /// Returns the number of seconds since the user last moved the mouse etc /// - public static uint SecondsSinceComputerActive => Internal.GetSecondsSinceComputerActive(); + public static uint SecondsSinceComputerActive => Internal?.GetSecondsSinceComputerActive() ?? 0; // the universe this client is connecting to - public static Universe ConnectedUniverse => Internal.GetConnectedUniverse(); + public static Universe ConnectedUniverse => Internal?.GetConnectedUniverse() ?? Universe.Invalid; /// /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) /// - public static DateTime SteamServerTime => Epoch.ToDateTime( Internal.GetServerRealTime() ); + public static DateTime SteamServerTime => Internal != null ? Epoch.ToDateTime( Internal.GetServerRealTime() ) : default; /// /// returns the 2 digit ISO 3166-1-alpha-2 format country code this client is running in (as looked up via an IP-to-location database) /// e.g "US" or "UK". /// - public static string IpCountry => Internal.GetIPCountry(); + public static string? IpCountry => Internal?.GetIPCountry(); /// /// returns true if the image exists, and the buffer was successfully filled out @@ -89,7 +89,7 @@ namespace Steamworks { width = 0; height = 0; - return Internal.GetImageSize( image, ref width, ref height ); + return Internal != null && Internal.GetImageSize( image, ref width, ref height ); } /// @@ -109,7 +109,7 @@ namespace Steamworks var buf = Helpers.TakeBuffer( (int) size ); - if ( !Internal.GetImageRGBA( image, buf, (int)size ) ) + if ( Internal is null || !Internal.GetImageRGBA( image, buf, (int)size ) ) return null; i.Data = new byte[size]; @@ -120,12 +120,12 @@ namespace Steamworks /// /// Returns true if we're using a battery (ie, a laptop not plugged in) /// - public static bool UsingBatteryPower => Internal.GetCurrentBatteryPower() != 255; + public static bool UsingBatteryPower => Internal != null && Internal.GetCurrentBatteryPower() != 255; /// /// Returns battery power [0-1] /// - public static float CurrentBatteryPower => Math.Min( Internal.GetCurrentBatteryPower() / 100, 1.0f ); + public static float CurrentBatteryPower => Math.Min( (Internal?.GetCurrentBatteryPower() ?? 0f) / 100, 1.0f ); static NotificationPosition overlayNotificationPosition = NotificationPosition.BottomRight; @@ -140,7 +140,7 @@ namespace Steamworks set { overlayNotificationPosition = value; - Internal.SetOverlayNotificationPosition( value ); + Internal?.SetOverlayNotificationPosition( value ); } } @@ -148,7 +148,7 @@ namespace Steamworks /// Returns true if the overlay is running and the user can access it. The overlay process could take a few seconds to /// start and hook the game process, so this function will initially return false while the overlay is loading. /// - public static bool IsOverlayEnabled => Internal.IsOverlayEnabled(); + public static bool IsOverlayEnabled => Internal != null && Internal.IsOverlayEnabled(); /// /// Normally this call is unneeded if your game has a constantly running frame loop that calls the @@ -161,7 +161,7 @@ namespace Steamworks /// in that case, and then you can check for this periodically (roughly 33hz is desirable) and make sure you /// refresh the screen with Present or SwapBuffers to allow the overlay to do it's work. /// - public static bool DoesOverlayNeedPresent => Internal.BOverlayNeedsPresent(); + public static bool DoesOverlayNeedPresent => Internal != null && Internal.BOverlayNeedsPresent(); /// /// Asynchronous call to check if an executable file has been signed using the public key set on the signing tab @@ -169,6 +169,8 @@ namespace Steamworks /// public static async Task CheckFileSignatureAsync( string filename ) { + if (Internal is null) { throw new System.Exception( "SteamUtils not initialized" ); } + var r = await Internal.CheckFileSignature( filename ); if ( !r.HasValue ) @@ -184,7 +186,7 @@ namespace Steamworks /// public static bool ShowGamepadTextInput( GamepadTextInputMode inputMode, GamepadTextInputLineMode lineInputMode, string description, int maxChars, string existingText = "" ) { - return Internal.ShowGamepadTextInput( inputMode, lineInputMode, description, (uint)maxChars, existingText ); + return Internal != null && Internal.ShowGamepadTextInput( inputMode, lineInputMode, description, (uint)maxChars, existingText ); } /// @@ -192,6 +194,8 @@ namespace Steamworks /// public static string GetEnteredGamepadText() { + if (Internal is null) { return string.Empty; } + var len = Internal.GetEnteredGamepadTextLength(); if ( len == 0 ) return string.Empty; @@ -205,19 +209,19 @@ namespace Steamworks /// returns the language the steam client is running in, you probably want /// Apps.CurrentGameLanguage instead, this is for very special usage cases /// - public static string SteamUILanguage => Internal.GetSteamUILanguage(); + public static string? SteamUILanguage => Internal?.GetSteamUILanguage(); /// /// returns true if Steam itself is running in VR mode /// - public static bool IsSteamRunningInVR => Internal.IsSteamRunningInVR(); + public static bool IsSteamRunningInVR => Internal != null && Internal.IsSteamRunningInVR(); /// /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition /// public static void SetOverlayNotificationInset( int x, int y ) { - Internal.SetOverlayNotificationInset( x, y ); + Internal?.SetOverlayNotificationInset( x, y ); } /// @@ -225,13 +229,13 @@ namespace Steamworks /// Games much be launched through the Steam client to enable the Big Picture overlay. During development, /// a game can be added as a non-steam game to the developers library to test this feature /// - public static bool IsSteamInBigPictureMode => Internal.IsSteamInBigPictureMode(); + public static bool IsSteamInBigPictureMode => Internal != null && Internal.IsSteamInBigPictureMode(); /// /// ask SteamUI to create and render its OpenVR dashboard /// - public static void StartVRDashboard() => Internal.StartVRDashboard(); + public static void StartVRDashboard() => Internal?.StartVRDashboard(); /// /// Set whether the HMD content will be streamed via Steam In-Home Streaming @@ -242,24 +246,24 @@ namespace Steamworks /// public static bool VrHeadsetStreaming { - get => Internal.IsVRHeadsetStreamingEnabled(); + get => Internal != null && Internal.IsVRHeadsetStreamingEnabled(); set { - Internal.SetVRHeadsetStreamingEnabled( value ); + Internal?.SetVRHeadsetStreamingEnabled( value ); } } internal static bool IsCallComplete( SteamAPICall_t call, out bool failed ) { failed = false; - return Internal.IsAPICallCompleted( call, ref failed ); + return Internal != null && Internal.IsAPICallCompleted( call, ref failed ); } /// /// Returns whether this steam client is a Steam China specific client, vs the global client /// - public static bool IsSteamChinaLauncher => Internal.IsSteamChinaLauncher(); + public static bool IsSteamChinaLauncher => Internal != null && Internal.IsSteamChinaLauncher(); } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamVideo.cs b/Libraries/Facepunch.Steamworks/SteamVideo.cs index c6eaf438c..f3ec5fabe 100644 --- a/Libraries/Facepunch.Steamworks/SteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/SteamVideo.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamVideo : SteamClientClass { - internal static ISteamVideo Internal => Interface as ISteamVideo; + internal static ISteamVideo? Internal => Interface as ISteamVideo; internal override void InitializeInterface( bool server ) { @@ -26,8 +26,8 @@ namespace Steamworks Dispatch.Install( x => OnBroadcastStopped?.Invoke( x.Result ) ); } - public static event Action OnBroadcastStarted; - public static event Action OnBroadcastStopped; + public static event Action? OnBroadcastStarted; + public static event Action? OnBroadcastStopped; /// /// Return true if currently using Steam's live broadcasting @@ -37,7 +37,7 @@ namespace Steamworks get { int viewers = 0; - return Internal.IsBroadcasting( ref viewers ); + return Internal != null && Internal.IsBroadcasting( ref viewers ); } } @@ -50,7 +50,7 @@ namespace Steamworks { int viewers = 0; - if ( !Internal.IsBroadcasting( ref viewers ) ) + if ( Internal is null || !Internal.IsBroadcasting( ref viewers ) ) return 0; return viewers; diff --git a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs index 9f0feeaf2..eb731ad94 100644 --- a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs +++ b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs @@ -74,12 +74,10 @@ namespace Steamworks m_RulesFailedToRespond = onRulesFailedToRespond; m_RulesRefreshComplete = onRulesRefreshComplete; - m_VTable = new VTable() - { - m_VTRulesResponded = InternalOnRulesResponded, - m_VTRulesFailedToRespond = InternalOnRulesFailedToRespond, - m_VTRulesRefreshComplete = InternalOnRulesRefreshComplete - }; + m_VTable = new VTable( + mVtRulesResponded: InternalOnRulesResponded, + mVtRulesFailedToRespond: InternalOnRulesFailedToRespond, + mVtRulesRefreshComplete: InternalOnRulesRefreshComplete); m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); Marshal.StructureToPtr(m_VTable, m_pVTable, false); @@ -153,13 +151,20 @@ namespace Steamworks private class VTable { [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesResponded m_VTRulesResponded; + public readonly InternalRulesResponded m_VTRulesResponded; [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesFailedToRespond m_VTRulesFailedToRespond; + public readonly InternalRulesFailedToRespond m_VTRulesFailedToRespond; [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesRefreshComplete m_VTRulesRefreshComplete; + public readonly InternalRulesRefreshComplete m_VTRulesRefreshComplete; + + public VTable(InternalRulesResponded mVtRulesResponded, InternalRulesFailedToRespond mVtRulesFailedToRespond, InternalRulesRefreshComplete mVtRulesRefreshComplete) + { + m_VTRulesResponded = mVtRulesResponded; + m_VTRulesFailedToRespond = mVtRulesFailedToRespond; + m_VTRulesRefreshComplete = mVtRulesRefreshComplete; + } } public static explicit operator System.IntPtr(SteamMatchmakingRulesResponse that) diff --git a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs index 04ac43871..963596c7e 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs @@ -25,16 +25,16 @@ namespace Steamworks.Data get { var state = false; - SteamUserStats.Internal.GetAchievement( Value, ref state ); + SteamUserStats.Internal?.GetAchievement( Value, ref state ); return state; } } public string Identifier => Value; - public string Name => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "name" ); + public string? Name => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "name" ); - public string Description => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "desc" ); + public string? Description => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "desc" ); /// @@ -47,7 +47,7 @@ namespace Steamworks.Data var state = false; uint time = 0; - if ( !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state ) return null; return Epoch.ToDateTime( time ); @@ -60,6 +60,7 @@ namespace Steamworks.Data /// public Image? GetIcon() { + if (SteamUserStats.Internal is null) { return null; } return SteamUtils.GetImage( SteamUserStats.Internal.GetAchievementIcon( Value ) ); } @@ -69,6 +70,7 @@ namespace Steamworks.Data /// public async Task GetIconAsync( int timeout = 5000 ) { + if (SteamUserStats.Internal is null) { return null; } var i = SteamUserStats.Internal.GetAchievementIcon( Value ); if ( i != 0 ) return SteamUtils.GetImage( i ); @@ -115,7 +117,7 @@ namespace Steamworks.Data { float pct = 0; - if ( !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) ) return -1.0f; return pct / 100.0f; @@ -127,6 +129,8 @@ namespace Steamworks.Data /// public bool Trigger( bool apply = true ) { + if (SteamUserStats.Internal is null) { return false; } + var r = SteamUserStats.Internal.SetAchievement( Value ); if ( apply && r ) @@ -142,6 +146,7 @@ namespace Steamworks.Data /// public bool Clear() { + if (SteamUserStats.Internal is null) { return false; } return SteamUserStats.Internal.ClearAchievement( Value ); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Clan.cs b/Libraries/Facepunch.Steamworks/Structs/Clan.cs index 6bcdda826..e6cca31d5 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Clan.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Clan.cs @@ -14,20 +14,20 @@ namespace Steamworks Id = id; } - public string Name => SteamFriends.Internal.GetClanName(Id); + public string? Name => SteamFriends.Internal?.GetClanName(Id); - public string Tag => SteamFriends.Internal.GetClanTag(Id); + public string? Tag => SteamFriends.Internal?.GetClanTag(Id); - public int ChatMemberCount => SteamFriends.Internal.GetClanChatMemberCount(Id); + public int ChatMemberCount => SteamFriends.Internal?.GetClanChatMemberCount(Id) ?? 0; - public Friend Owner => new Friend(SteamFriends.Internal.GetClanOwner(Id)); + public Friend Owner => new Friend(SteamFriends.Internal?.GetClanOwner(Id) ?? 0); - public bool Public => SteamFriends.Internal.IsClanPublic(Id); + public bool Public => SteamFriends.Internal != null && SteamFriends.Internal.IsClanPublic(Id); /// /// Is the clan an official game group? /// - public bool Official => SteamFriends.Internal.IsClanOfficialGameGroup(Id); + public bool Official => SteamFriends.Internal != null && SteamFriends.Internal.IsClanOfficialGameGroup(Id); /// /// Asynchronously fetches the officer list for a given clan @@ -35,14 +35,18 @@ namespace Steamworks /// Whether the request was successful or not public async Task RequestOfficerList() { + if (SteamFriends.Internal is null) { return false; } var req = await SteamFriends.Internal.RequestClanOfficerList(Id); return req.HasValue && req.Value.Success != 0x0; } public IEnumerable GetOfficers() { - for (int i = 0; i < SteamFriends.Internal.GetClanOfficerCount(Id); i++) + if (SteamFriends.Internal is null) { yield break; } + var officerCount = SteamFriends.Internal.GetClanOfficerCount(Id); + for (int i = 0; i < officerCount; i++) { + if (SteamFriends.Internal is null) { yield break; } yield return new Friend(SteamFriends.Internal.GetClanOfficerByIndex(Id, i)); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Controller.cs b/Libraries/Facepunch.Steamworks/Structs/Controller.cs index f694ecd2c..6fe355bd8 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Controller.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Controller.cs @@ -14,7 +14,7 @@ namespace Steamworks } public ulong Id => Handle.Value; - public InputType InputType => SteamInput.Internal.GetInputTypeForHandle( Handle ); + public InputType InputType => SteamInput.Internal?.GetInputTypeForHandle( Handle ) ?? InputType.Unknown; /// /// Reconfigure the controller to use the specified action set (ie 'Menu', 'Walk' or 'Drive') @@ -23,12 +23,12 @@ namespace Steamworks /// public string ActionSet { - set => SteamInput.Internal.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) ); + set => SteamInput.Internal?.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) ); } - public void DeactivateLayer( string layer ) => SteamInput.Internal.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); - public void ActivateLayer( string layer ) => SteamInput.Internal.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); - public void ClearLayers() => SteamInput.Internal.DeactivateAllActionSetLayers( Handle ); + public void DeactivateLayer( string layer ) => SteamInput.Internal?.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); + public void ActivateLayer( string layer ) => SteamInput.Internal?.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); + public void ClearLayers() => SteamInput.Internal?.DeactivateAllActionSetLayers( Handle ); /// @@ -36,7 +36,7 @@ namespace Steamworks /// public DigitalState GetDigitalState( string actionName ) { - return SteamInput.Internal.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) ); + return SteamInput.Internal?.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) ) ?? default; } /// @@ -44,7 +44,7 @@ namespace Steamworks /// public AnalogState GetAnalogState( string actionName ) { - return SteamInput.Internal.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) ); + return SteamInput.Internal?.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) ) ?? default; } diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 510227bbf..90a1df26f 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -73,16 +73,16 @@ namespace Steamworks - public Relationship Relationship => SteamFriends.Internal.GetFriendRelationship( Id ); - public FriendState State => SteamFriends.Internal.GetFriendPersonaState( Id ); - public string Name => SteamFriends.Internal.GetFriendPersonaName( Id ); + public Relationship Relationship => SteamFriends.Internal?.GetFriendRelationship( Id ) ?? Relationship.None; + public FriendState State => SteamFriends.Internal?.GetFriendPersonaState( Id ) ?? FriendState.Offline; + public string? Name => SteamFriends.Internal?.GetFriendPersonaName( Id ); public IEnumerable NameHistory { get { for( int i=0; i<32; i++ ) { - var n = SteamFriends.Internal.GetFriendPersonaNameHistory( Id, i ); + var n = SteamFriends.Internal?.GetFriendPersonaNameHistory( Id, i ); if ( string.IsNullOrEmpty( n ) ) break; @@ -91,7 +91,7 @@ namespace Steamworks } } - public int SteamLevel => SteamFriends.Internal.GetFriendSteamLevel( Id ); + public int SteamLevel => SteamFriends.Internal?.GetFriendSteamLevel( Id ) ?? 0; @@ -100,7 +100,7 @@ namespace Steamworks get { FriendGameInfo_t gameInfo = default; - if ( !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) ) + if ( SteamFriends.Internal is null || !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) ) return null; return FriendGameInfo.From( gameInfo ); @@ -109,7 +109,7 @@ namespace Steamworks public bool IsIn( SteamId group_or_room ) { - return SteamFriends.Internal.IsUserInSource( Id, group_or_room ); + return SteamFriends.Internal != null && SteamFriends.Internal.IsUserInSource( Id, group_or_room ); } public struct FriendGameInfo @@ -161,9 +161,9 @@ namespace Steamworks return await SteamFriends.GetLargeAvatarAsync( Id ); } - public string GetRichPresence( string key ) + public string? GetRichPresence( string key ) { - var val = SteamFriends.Internal.GetFriendRichPresence( Id, key ); + var val = SteamFriends.Internal?.GetFriendRichPresence( Id, key ); if ( string.IsNullOrEmpty( val ) ) return null; return val; } @@ -173,7 +173,7 @@ namespace Steamworks /// public bool InviteToGame( string Text ) { - return SteamFriends.Internal.InviteUserToGame( Id, Text ); + return SteamFriends.Internal != null && SteamFriends.Internal.InviteUserToGame( Id, Text ); } /// @@ -181,7 +181,7 @@ namespace Steamworks /// public bool SendMessage( string message ) { - return SteamFriends.Internal.ReplyToFriendMessage( Id, message ); + return SteamFriends.Internal != null && SteamFriends.Internal.ReplyToFriendMessage( Id, message ); } @@ -191,8 +191,9 @@ namespace Steamworks /// True if successful, False if failure public async Task RequestUserStatsAsync() { + if (SteamUserStats.Internal is null) { return false; } var result = await SteamUserStats.Internal.RequestUserStats( Id ); - return result.HasValue && result.Value.Result == Result.OK; + return result?.Result == Result.OK; } /// @@ -205,7 +206,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) return defult; return val; @@ -221,7 +222,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) return defult; return val; @@ -237,7 +238,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) ) return defult; return val; @@ -253,7 +254,7 @@ namespace Steamworks bool val = false; uint time = 0; - if ( !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val ) return DateTime.MinValue; return Epoch.ToDateTime( time ); diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs index 815094000..25afbd570 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs @@ -8,7 +8,7 @@ namespace Steamworks public class InventoryDef : IEquatable { internal InventoryDefId _id; - internal Dictionary _properties; + internal Dictionary? _properties; public InventoryDef( InventoryDefId defId ) { @@ -20,32 +20,32 @@ namespace Steamworks /// /// Shortcut to call GetProperty( "name" ) /// - public string Name => GetProperty( "name" ); + public string? Name => GetProperty( "name" ); /// /// Shortcut to call GetProperty( "description" ) /// - public string Description => GetProperty( "description" ); + public string? Description => GetProperty( "description" ); /// /// Shortcut to call GetProperty( "icon_url" ) /// - public string IconUrl => GetProperty( "icon_url" ); + public string? IconUrl => GetProperty( "icon_url" ); /// /// Shortcut to call GetProperty( "icon_url_large" ) /// - public string IconUrlLarge => GetProperty( "icon_url_large" ); + public string? IconUrlLarge => GetProperty( "icon_url_large" ); /// /// Shortcut to call GetProperty( "price_category" ) /// - public string PriceCategory => GetProperty( "price_category" ); + public string? PriceCategory => GetProperty( "price_category" ); /// /// Shortcut to call GetProperty( "type" ) /// - public string Type => GetProperty( "type" ); + public string? Type => GetProperty( "type" ); /// /// Returns true if this is an item that generates an item, rather @@ -56,12 +56,12 @@ namespace Steamworks /// /// Shortcut to call GetProperty( "exchange" ) /// - public string ExchangeSchema => GetProperty( "exchange" ); + public string? ExchangeSchema => GetProperty( "exchange" ); /// /// Get a list of exchanges that are available to make this item /// - public InventoryRecipe[] GetRecipes() + public InventoryRecipe[]? GetRecipes() { if ( string.IsNullOrEmpty( ExchangeSchema ) ) return null; @@ -93,19 +93,19 @@ namespace Steamworks /// /// Get a specific property by name /// - public string GetProperty( string name ) + public string? GetProperty( string? name ) { - if ( _properties!= null && _properties.TryGetValue( name, out string val ) ) + if ( _properties != null && name != null && _properties.TryGetValue( name, out string val ) ) return val; uint _ = (uint)Helpers.MemoryBufferSize; - if ( !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) ) return null; - + if (name == null) //return keys string return vl; - + if ( _properties == null ) _properties = new Dictionary(); @@ -119,9 +119,9 @@ namespace Steamworks /// public bool GetBoolProperty( string name ) { - string val = GetProperty( name ); + string? val = GetProperty( name ); - if ( val.Length == 0 ) return false; + if ( string.IsNullOrEmpty(val) ) return false; if ( val[0] == '0' || val[0] == 'F' || val[0] == 'f' ) return false; return true; @@ -130,9 +130,9 @@ namespace Steamworks /// /// Read a raw property from the definition schema /// - public T GetProperty( string name ) + public T? GetProperty( string name ) { - string val = GetProperty( name ); + string? val = GetProperty( name ); if ( string.IsNullOrEmpty( val ) ) return default; @@ -150,16 +150,16 @@ namespace Steamworks /// /// Gets a list of all properties on this item /// - public IEnumerable> Properties + public IEnumerable> Properties { get { - var list = GetProperty( null ); + var list = GetProperty( null ) ?? ""; var keys = list.Split( ',' ); foreach ( var key in keys ) { - yield return new KeyValuePair( key, GetProperty( key ) ); + yield return new KeyValuePair( key, GetProperty( key ) ); } } } @@ -174,7 +174,7 @@ namespace Steamworks ulong curprice = 0; ulong baseprice = 0; - if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) return 0; return (int) curprice; @@ -194,7 +194,7 @@ namespace Steamworks ulong curprice = 0; ulong baseprice = 0; - if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) return 0; return (int)baseprice; @@ -203,12 +203,12 @@ namespace Steamworks public string LocalBasePriceFormatted => Utility.FormatPrice( SteamInventory.Currency, LocalPrice / 100.0 ); - InventoryRecipe[] _recContaining; + InventoryRecipe[]? _recContaining; /// /// Return a list of recepies that contain this item /// - public InventoryRecipe[] GetRecipesContainingThis() + public InventoryRecipe[]? GetRecipesContainingThis() { if ( _recContaining != null ) return _recContaining; @@ -221,17 +221,17 @@ namespace Steamworks return _recContaining; } - public static bool operator ==( InventoryDef a, InventoryDef b ) + public static bool operator ==( InventoryDef? a, InventoryDef? b ) { if ( Object.ReferenceEquals( a, null ) ) return Object.ReferenceEquals( b, null ); return a.Equals( b ); } - public static bool operator !=( InventoryDef a, InventoryDef b ) => !(a == b); + public static bool operator !=( InventoryDef? a, InventoryDef? b ) => !(a == b); public override bool Equals( object p ) => this.Equals( (InventoryDef)p ); public override int GetHashCode() => Id.GetHashCode(); - public bool Equals( InventoryDef p ) + public bool Equals( InventoryDef? p ) { if ( p == null ) return false; return p.Id == Id; diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs index 886ed9be9..aeee9a5e3 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs @@ -11,7 +11,7 @@ namespace Steamworks internal InventoryDefId _def; internal SteamItemFlags _flags; internal ushort _quantity; - internal Dictionary _properties; + internal Dictionary? _properties; public InventoryItemId Id => _id; @@ -19,13 +19,13 @@ namespace Steamworks public int Quantity => _quantity; - public InventoryDef Def => SteamInventory.FindDefinition( DefId ); + public InventoryDef? Def => SteamInventory.FindDefinition( DefId ); /// /// Only available if the result set was created with the getproperties /// - public Dictionary Properties => _properties; + public Dictionary? Properties => _properties; /// /// This item is account-locked and cannot be traded or given away. @@ -54,7 +54,7 @@ namespace Steamworks public async Task ConsumeAsync( int amount = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -66,7 +66,7 @@ namespace Steamworks public async Task SplitStackAsync( int quantity = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -78,7 +78,7 @@ namespace Steamworks public async Task AddAsync( InventoryItem add, int quantity = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -98,11 +98,11 @@ namespace Steamworks return i; } - internal static Dictionary GetProperties( SteamInventoryResult_t result, int index ) + internal static Dictionary? GetProperties( SteamInventoryResult_t result, int index ) { var strlen = (uint) Helpers.MemoryBufferSize; - if ( !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) ) return null; var props = new Dictionary(); @@ -151,7 +151,7 @@ namespace Steamworks /// Tries to get the origin property. Need properties for this to work. /// Will return a string like "market" /// - public string Origin + public string? Origin { get { diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs index 1305eb8d0..21440664d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs @@ -22,7 +22,7 @@ namespace Steamworks /// If we don't know about this item definition this might be null. /// In which case, DefinitionId should still hold the correct id. /// - public InventoryDef Definition; + public InventoryDef? Definition; /// /// The amount of this item needed. Generally this will be 1. diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs index 5518ba4ed..8c58353b1 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs @@ -23,7 +23,7 @@ namespace Steamworks { uint cnt = 0; - if ( !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) ) return 0; return (int) cnt; @@ -36,17 +36,17 @@ namespace Steamworks /// public bool BelongsTo( SteamId steamId ) { - return SteamInventory.Internal.CheckResultSteamID( _id, steamId ); + return SteamInventory.Internal != null && SteamInventory.Internal.CheckResultSteamID( _id, steamId ); } - public InventoryItem[] GetItems( bool includeProperties = false ) + public InventoryItem[]? GetItems( bool includeProperties = false ) { uint cnt = (uint) ItemCount; if ( cnt <= 0 ) return null; var pOutItemsArray = new SteamItemDetails_t[cnt]; - if ( !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) ) return null; var items = new InventoryItem[cnt]; @@ -69,7 +69,7 @@ namespace Steamworks { if ( _id.Value == -1 ) return; - SteamInventory.Internal.DestroyResult( _id ); + SteamInventory.Internal?.DestroyResult( _id ); } internal static async Task GetAsync( SteamInventoryResult_t sresult ) @@ -77,7 +77,7 @@ namespace Steamworks var _result = Result.Pending; while ( _result == Result.Pending ) { - _result = SteamInventory.Internal.GetResultStatus( sresult ); + _result = SteamInventory.Internal?.GetResultStatus( sresult ) ?? Result.Fail; await Task.Delay( 10 ); } @@ -97,11 +97,11 @@ namespace Steamworks /// Results have a built-in timestamp which will be considered "expired" after an hour has elapsed.See DeserializeResult /// for expiration handling. /// - public unsafe byte[] Serialize() + public unsafe byte[]? Serialize() { uint size = 0; - if ( !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) ) return null; var data = new byte[size]; diff --git a/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs b/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs index ed7deb8c6..38c0dc1e0 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs @@ -14,10 +14,10 @@ namespace Steamworks.Data /// /// the name of a leaderboard /// - public string Name => SteamUserStats.Internal.GetLeaderboardName( Id ); - public LeaderboardSort Sort => SteamUserStats.Internal.GetLeaderboardSortMethod( Id ); - public LeaderboardDisplay Display => SteamUserStats.Internal.GetLeaderboardDisplayType( Id ); - public int EntryCount => SteamUserStats.Internal.GetLeaderboardEntryCount(Id); + public string? Name => SteamUserStats.Internal?.GetLeaderboardName( Id ); + public LeaderboardSort Sort => SteamUserStats.Internal?.GetLeaderboardSortMethod( Id ) ?? default; + public LeaderboardDisplay Display => SteamUserStats.Internal?.GetLeaderboardDisplayType( Id ) ?? default; + public int EntryCount => SteamUserStats.Internal?.GetLeaderboardEntryCount(Id) ?? 0; static int[] detailsBuffer = new int[64]; static int[] noDetails = Array.Empty(); @@ -25,8 +25,9 @@ namespace Steamworks.Data /// /// Submit your score and replace your old score even if it was better /// - public async Task ReplaceScore( int score, int[] details = null ) + public async Task ReplaceScore( int score, int[]? details = null ) { + if (SteamUserStats.Internal is null) { return null; } if ( details == null ) details = noDetails; var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.ForceUpdate, score, details, details.Length ); @@ -38,8 +39,9 @@ namespace Steamworks.Data /// /// Submit your new score, but won't replace your high score if it's lower /// - public async Task SubmitScoreAsync( int score, int[] details = null ) + public async Task SubmitScoreAsync( int score, int[]? details = null ) { + if (SteamUserStats.Internal is null) { return null; } if ( details == null ) details = noDetails; var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.KeepBest, score, details, details.Length ); @@ -53,6 +55,7 @@ namespace Steamworks.Data /// public async Task AttachUgc( Ugc file ) { + if (SteamUserStats.Internal is null) { return Result.Fail; } var r = await SteamUserStats.Internal.AttachLeaderboardUGC( Id, file.Handle ); if ( !r.HasValue ) return Result.Fail; @@ -62,8 +65,9 @@ namespace Steamworks.Data /// /// Fetches leaderboard entries for an arbitrary set of users on a specified leaderboard. /// - public async Task GetScoresForUsersAsync( SteamId[] users ) + public async Task GetScoresForUsersAsync( SteamId[]? users ) { + if (SteamUserStats.Internal is null) { return null; } if ( users == null || users.Length == 0 ) return null; @@ -77,8 +81,9 @@ namespace Steamworks.Data /// /// Used to query for a sequential range of leaderboard entries by leaderboard Sort. /// - public async Task GetScoresAsync( int count, int offset = 1 ) + public async Task GetScoresAsync( int count, int offset = 1 ) { + if (SteamUserStats.Internal is null) { return null; } if ( offset <= 0 ) throw new System.ArgumentException( "Should be 1+", nameof( offset ) ); var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Global, offset, offset + count - 1 ); @@ -94,8 +99,9 @@ namespace Steamworks.Data /// For example, if the user is #1 on the leaderboard and start is set to -2, end is set to 2, Steam will return the first /// 5 entries in the leaderboard. If The current user has no entry, this will return null. /// - public async Task GetScoresAroundUserAsync( int start = -10, int end = 10 ) + public async Task GetScoresAroundUserAsync( int start = -10, int end = 10 ) { + if (SteamUserStats.Internal is null) { return null; } var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.GlobalAroundUser, start, end ); if ( !r.HasValue ) return null; @@ -106,8 +112,9 @@ namespace Steamworks.Data /// /// Used to retrieve all leaderboard entries for friends of the current user /// - public async Task GetScoresFromFriendsAsync() + public async Task GetScoresFromFriendsAsync() { + if (SteamUserStats.Internal is null) { return null; } var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Friends, 0, 0 ); if ( !r.HasValue ) return null; @@ -116,8 +123,9 @@ namespace Steamworks.Data } #region util - internal async Task LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r ) + internal async Task LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r ) { + if (SteamUserStats.Internal is null) { return null; } if ( r.CEntryCount <= 0 ) return null; @@ -142,6 +150,8 @@ namespace Steamworks.Data bool gotAll = false; while ( !gotAll ) { + if (SteamFriends.Internal is null) { return; } + gotAll = true; foreach ( var entry in entries ) diff --git a/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs b/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs index 82eb26fb7..6758da17d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs @@ -7,7 +7,7 @@ namespace Steamworks.Data public Friend User; public int GlobalRank; public int Score; - public int[] Details; + public int[]? Details; // UGCHandle_t m_hUGC internal static LeaderboardEntry From( LeaderboardEntry_t e, int[] detailsBuffer ) diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 2b0fdb57a..397e82350 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -23,6 +23,8 @@ namespace Steamworks.Data /// public async Task Join() { + if (SteamMatchmaking.Internal is null) { return RoomEnter.Error; } + var result = await SteamMatchmaking.Internal.JoinLobby( Id ); if ( !result.HasValue ) return RoomEnter.Error; @@ -35,7 +37,7 @@ namespace Steamworks.Data /// public void Leave() { - SteamMatchmaking.Internal.LeaveLobby( Id ); + SteamMatchmaking.Internal?.LeaveLobby( Id ); } /// @@ -45,13 +47,13 @@ namespace Steamworks.Data /// public bool InviteFriend( SteamId steamid ) { - return SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid ); } /// /// returns the number of users in the specified lobby /// - public int MemberCount => SteamMatchmaking.Internal.GetNumLobbyMembers( Id ); + public int MemberCount => SteamMatchmaking.Internal?.GetNumLobbyMembers( Id ) ?? 0; /// /// Returns current members. Need to be in the lobby to see the users. @@ -62,6 +64,7 @@ namespace Steamworks.Data { for( int i = 0; i < MemberCount; i++ ) { + if (SteamMatchmaking.Internal is null) { break; } yield return new Friend( SteamMatchmaking.Internal.GetLobbyMemberByIndex( Id, i ) ); } } @@ -71,9 +74,9 @@ namespace Steamworks.Data /// /// Get data associated with this lobby /// - public string GetData( string key ) + public string? GetData( string key ) { - return SteamMatchmaking.Internal.GetLobbyData( Id, key ); + return SteamMatchmaking.Internal?.GetLobbyData( Id, key ); } /// @@ -84,7 +87,7 @@ namespace Steamworks.Data if ( key.Length > 255 ) throw new System.ArgumentException( "Key should be < 255 chars", nameof( key ) ); if ( value.Length > 8192 ) throw new System.ArgumentException( "Value should be < 8192 chars", nameof( key ) ); - return SteamMatchmaking.Internal.SetLobbyData( Id, key, value ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyData( Id, key, value ); } /// @@ -92,7 +95,7 @@ namespace Steamworks.Data /// public bool DeleteData( string key ) { - return SteamMatchmaking.Internal.DeleteLobbyData( Id, key ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.DeleteLobbyData( Id, key ); } /// @@ -102,10 +105,11 @@ namespace Steamworks.Data { get { - var cnt = SteamMatchmaking.Internal.GetLobbyDataCount( Id ); + var cnt = SteamMatchmaking.Internal?.GetLobbyDataCount( Id ) ?? 0; for ( int i =0; i( a, b ); @@ -117,9 +121,9 @@ namespace Steamworks.Data /// /// Gets per-user metadata for someone in this lobby /// - public string GetMemberData( Friend member, string key ) + public string? GetMemberData( Friend member, string key ) { - return SteamMatchmaking.Internal.GetLobbyMemberData( Id, member.Id, key ); + return SteamMatchmaking.Internal?.GetLobbyMemberData( Id, member.Id, key ); } /// @@ -127,7 +131,7 @@ namespace Steamworks.Data /// public void SetMemberData( string key, string value ) { - SteamMatchmaking.Internal.SetLobbyMemberData( Id, key, value ); + SteamMatchmaking.Internal?.SetLobbyMemberData( Id, key, value ); } /// @@ -148,7 +152,7 @@ namespace Steamworks.Data { fixed ( byte* ptr = data ) { - return SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); } } @@ -163,7 +167,7 @@ namespace Steamworks.Data /// public bool Refresh() { - return SteamMatchmaking.Internal.RequestLobbyData( Id ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.RequestLobbyData( Id ); } /// @@ -172,33 +176,33 @@ namespace Steamworks.Data /// public int MaxMembers { - get => SteamMatchmaking.Internal.GetLobbyMemberLimit( Id ); - set => SteamMatchmaking.Internal.SetLobbyMemberLimit( Id, value ); + get => SteamMatchmaking.Internal?.GetLobbyMemberLimit( Id ) ?? 0; + set => SteamMatchmaking.Internal?.SetLobbyMemberLimit( Id, value ); } public bool SetPublic() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); } public bool SetPrivate() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); } public bool SetInvisible() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); } public bool SetFriendsOnly() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); } public bool SetJoinable( bool b ) { - return SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); } /// @@ -211,7 +215,7 @@ namespace Steamworks.Data if ( !steamServer.IsValid ) throw new ArgumentException( $"SteamId for server is invalid" ); - SteamMatchmaking.Internal.SetLobbyGameServer( Id, 0, 0, steamServer ); + SteamMatchmaking.Internal?.SetLobbyGameServer( Id, 0, 0, steamServer ); } /// @@ -224,7 +228,7 @@ namespace Steamworks.Data if ( !IPAddress.TryParse( ip, out IPAddress add ) ) throw new ArgumentException( $"IP address for server is invalid" ); - SteamMatchmaking.Internal.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() ); + SteamMatchmaking.Internal?.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() ); } /// @@ -233,7 +237,7 @@ namespace Steamworks.Data /// public bool GetGameServer( ref uint ip, ref ushort port, ref SteamId serverId ) { - return SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId ); } /// @@ -241,8 +245,8 @@ namespace Steamworks.Data /// public Friend Owner { - get => new Friend( SteamMatchmaking.Internal.GetLobbyOwner( Id ) ); - set => SteamMatchmaking.Internal.SetLobbyOwner( Id, value.Id ); + get => new Friend( SteamMatchmaking.Internal?.GetLobbyOwner( Id ) ?? 0 ); + set => SteamMatchmaking.Internal?.SetLobbyOwner( Id, value.Id ); } /// diff --git a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs index 1401e5820..4a55ef640 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs @@ -197,6 +197,8 @@ namespace Steamworks.Data void ApplyFilters() { + if (SteamMatchmaking.Internal is null) { return; } + if ( distance.HasValue ) { SteamMatchmaking.Internal.AddRequestLobbyListDistanceFilter( distance.Value ); @@ -251,8 +253,10 @@ namespace Steamworks.Data /// /// Run the query, get the matching lobbies /// - public async Task RequestAsync() + public async Task RequestAsync() { + if (SteamMatchmaking.Internal is null) { return null; } + await Task.Yield(); ApplyFilters(); diff --git a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs index b88904221..5254c9f50 100644 --- a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs +++ b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs @@ -5,7 +5,7 @@ namespace Steamworks { public struct PartyBeacon { - static ISteamParties Internal => SteamParties.Internal; + static ISteamParties? Internal => SteamParties.Internal; internal PartyBeaconID_t Id; @@ -18,7 +18,7 @@ namespace Steamworks { var owner = default( SteamId ); var location = default( SteamPartyBeaconLocation_t ); - Internal.GetBeaconDetails( Id, ref owner, ref location, out _ ); + Internal?.GetBeaconDetails( Id, ref owner, ref location, out _ ); return owner; } } @@ -26,13 +26,14 @@ namespace Steamworks /// /// Creator of the beacon /// - public string MetaData + public string? MetaData { get { var owner = default( SteamId ); var location = default( SteamPartyBeaconLocation_t ); - _ = Internal.GetBeaconDetails( Id, ref owner, ref location, out var strVal ); + string? strVal = null; + _ = Internal?.GetBeaconDetails( Id, ref owner, ref location, out strVal ); return strVal; } } @@ -41,8 +42,10 @@ namespace Steamworks /// Will attempt to join the party. If successful will return a connection string. /// If failed, will return null /// - public async Task JoinAsync() + public async Task JoinAsync() { + if (Internal is null) { return null; } + var result = await Internal.JoinParty( Id ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; @@ -56,7 +59,7 @@ namespace Steamworks /// public void OnReservationCompleted( SteamId steamid ) { - Internal.OnReservationCompleted( Id, steamid ); + Internal?.OnReservationCompleted( Id, steamid ); } /// @@ -66,7 +69,7 @@ namespace Steamworks /// public void CancelReservation( SteamId steamid ) { - Internal.CancelReservation( Id, steamid ); + Internal?.CancelReservation( Id, steamid ); } /// @@ -74,7 +77,7 @@ namespace Steamworks /// public bool Destroy() { - return Internal.DestroyBeacon( Id ); + return Internal != null && Internal.DestroyBeacon( Id ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs b/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs index f39693fce..655ab288d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs +++ b/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs @@ -23,16 +23,16 @@ namespace Steamworks.Data /// /// Get the SteamID of the connected user /// - public SteamId SteamId => SteamRemotePlay.Internal.GetSessionSteamID( Id ); + public SteamId SteamId => SteamRemotePlay.Internal?.GetSessionSteamID( Id ) ?? default; /// /// Get the name of the session client device /// - public string ClientName => SteamRemotePlay.Internal.GetSessionClientName( Id ); + public string? ClientName => SteamRemotePlay.Internal?.GetSessionClientName( Id ); /// /// Get the name of the session client device /// - public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal.GetSessionClientFormFactor( Id ); + public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal?.GetSessionClientFormFactor( Id ) ?? SteamDeviceFormFactor.Unknown; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs index 2d6e92010..a30b506e4 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs @@ -15,7 +15,7 @@ namespace Steamworks.Data /// public bool TagUser( SteamId user ) { - return SteamScreenshots.Internal.TagUser( Value, user ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagUser( Value, user ); } /// @@ -23,7 +23,7 @@ namespace Steamworks.Data /// public bool SetLocation( string location ) { - return SteamScreenshots.Internal.SetLocation( Value, location ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.SetLocation( Value, location ); } /// @@ -31,7 +31,7 @@ namespace Steamworks.Data /// public bool TagPublishedFile( PublishedFileId file ) { - return SteamScreenshots.Internal.TagPublishedFile( Value, file ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagPublishedFile( Value, file ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/Server.cs b/Libraries/Facepunch.Steamworks/Structs/Server.cs index 89ea2243a..5e74b0a35 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Server.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Server.cs @@ -9,11 +9,11 @@ namespace Steamworks.Data { public struct ServerInfo : IEquatable { - public string Name { get; set; } + public string? Name { get; set; } public int Ping { get; set; } - public string GameDir { get; set; } - public string Map { get; set; } - public string Description { get; set; } + public string? GameDir { get; set; } + public string? Map { get; set; } + public string? Description { get; set; } public uint AppId { get; set; } public int Players { get; set; } public int MaxPlayers { get; set; } @@ -22,19 +22,19 @@ namespace Steamworks.Data public bool Secure { get; set; } public uint LastTimePlayed { get; set; } public int Version { get; set; } - public string TagString { get; set; } + public string? TagString { get; set; } public ulong SteamId { get; set; } public uint AddressRaw { get; set; } - public IPAddress Address { get; set; } + public IPAddress? Address { get; set; } public int ConnectionPort { get; set; } public int QueryPort { get; set; } - string[] _tags; + string[]? _tags; /// /// Gets the individual tags for this server /// - public string[] Tags + public string[]? Tags { get { @@ -97,13 +97,13 @@ namespace Steamworks.Data /// public void AddToHistory() { - SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current ); + SteamMatchmaking.Internal?.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current ); } /// /// If this server responds to source engine style queries, we'll be able to get a list of rules here /// - public async Task> QueryRulesAsync() + public async Task?> QueryRulesAsync() { return await SourceServerQuery.GetRules( this ); } @@ -113,7 +113,7 @@ namespace Steamworks.Data /// public void RemoveFromHistory() { - SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); + SteamMatchmaking.Internal?.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); } /// @@ -121,7 +121,7 @@ namespace Steamworks.Data /// public void AddToFavourites() { - SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current ); + SteamMatchmaking.Internal?.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current ); } /// @@ -129,7 +129,7 @@ namespace Steamworks.Data /// public void RemoveFromFavourites() { - SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); + SteamMatchmaking.Internal?.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); } public bool Equals( ServerInfo other ) @@ -139,7 +139,7 @@ namespace Steamworks.Data public override int GetHashCode() { - return Address.GetHashCode() + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); + return (Address?.GetHashCode() ?? 0) + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs index 7664a9bb1..637c85c06 100644 --- a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs +++ b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs @@ -21,7 +21,7 @@ namespace Steamworks /// public struct SteamServerInit { - public IPAddress IpAddress; + public IPAddress? IpAddress; public ushort SteamPort; public ushort GamePort; public ushort QueryPort; diff --git a/Libraries/Facepunch.Steamworks/Structs/Stat.cs b/Libraries/Facepunch.Steamworks/Structs/Stat.cs index 559fb6954..505ffeb4b 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Stat.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Stat.cs @@ -26,7 +26,7 @@ namespace Steamworks.Data UserId = user; } - internal void LocalUserOnly( [CallerMemberName] string caller = null ) + internal void LocalUserOnly( [CallerMemberName] string? caller = null ) { if ( UserId == 0 ) return; throw new System.Exception( $"Stat.{caller} can only be called for the local user" ); @@ -36,7 +36,7 @@ namespace Steamworks.Data { double val = 0.0; - if ( SteamUserStats.Internal.GetGlobalStat( Name, ref val ) ) + if ( SteamUserStats.Internal != null && SteamUserStats.Internal.GetGlobalStat( Name, ref val ) ) return val; return 0; @@ -45,12 +45,14 @@ namespace Steamworks.Data public long GetGlobalInt() { long val = 0; - SteamUserStats.Internal.GetGlobalStat( Name, ref val ); + SteamUserStats.Internal?.GetGlobalStat( Name, ref val ); return val; } - public async Task GetGlobalIntDaysAsync( int days ) + public async Task GetGlobalIntDaysAsync( int days ) { + if (SteamUserStats.Internal is null) { return null; } + var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( result?.Result != Result.OK ) return null; @@ -64,8 +66,10 @@ namespace Steamworks.Data return r; } - public async Task GetGlobalFloatDays( int days ) + public async Task GetGlobalFloatDays( int days ) { + if (SteamUserStats.Internal is null) { return null; } + var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( result?.Result != Result.OK ) return null; @@ -85,14 +89,14 @@ namespace Steamworks.Data if ( UserId > 0 ) { - SteamUserStats.Internal.GetUserStat( UserId, Name, ref val ); + SteamUserStats.Internal?.GetUserStat( UserId, Name, ref val ); } else { - SteamUserStats.Internal.GetStat( Name, ref val ); + SteamUserStats.Internal?.GetStat( Name, ref val ); } - return 0; + return val; } public int GetInt() @@ -101,11 +105,11 @@ namespace Steamworks.Data if ( UserId > 0 ) { - SteamUserStats.Internal.GetUserStat( UserId, Name, ref val ); + SteamUserStats.Internal?.GetUserStat( UserId, Name, ref val ); } else { - SteamUserStats.Internal.GetStat( Name, ref val ); + SteamUserStats.Internal?.GetStat( Name, ref val ); } return val; @@ -114,13 +118,13 @@ namespace Steamworks.Data public bool Set( int val ) { LocalUserOnly(); - return SteamUserStats.Internal.SetStat( Name, val ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.SetStat( Name, val ); } public bool Set( float val ) { LocalUserOnly(); - return SteamUserStats.Internal.SetStat( Name, val ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.SetStat( Name, val ); } public bool Add( int val ) @@ -138,13 +142,13 @@ namespace Steamworks.Data public bool UpdateAverageRate( float count, float sessionlength ) { LocalUserOnly(); - return SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength ); } public bool Store() { LocalUserOnly(); - return SteamUserStats.Internal.StoreStats(); + return SteamUserStats.Internal != null && SteamUserStats.Internal.StoreStats(); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index 9acaf229c..1067141b5 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -47,25 +47,25 @@ namespace Steamworks.Ugc public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; } - public string Title { get; private set; } + public string? Title { get; private set; } public Editor WithTitle( string t ) { this.Title = t; return this; } - public string Description { get; private set; } + public string? Description { get; private set; } public Editor WithDescription( string t ) { this.Description = t; return this; } - string MetaData; + string? MetaData; public Editor WithMetaData( string t ) { this.MetaData = t; return this; } - string ChangeLog; + string? ChangeLog; public Editor WithChangeLog( string t ) { this.ChangeLog = t; return this; } - string Language; + string? Language; public Editor InLanguage( string t ) { this.Language = t; return this; } - public string PreviewFile { get; private set; } - public Editor WithPreviewFile( string t ) { this.PreviewFile = t; return this; } + public string? PreviewFile { get; private set; } + public Editor WithPreviewFile( string? t ) { this.PreviewFile = t; return this; } - public System.IO.DirectoryInfo ContentFolder { get; private set; } + public System.IO.DirectoryInfo? ContentFolder { get; private set; } public Editor WithContent( System.IO.DirectoryInfo t ) { this.ContentFolder = t; return this; } public Editor WithContent( string folderName ) { return WithContent( new System.IO.DirectoryInfo( folderName ) ); } @@ -73,9 +73,9 @@ namespace Steamworks.Ugc public Editor WithVisibility(Visibility visibility) { Visibility = visibility; return this; } - public List Tags { get; private set; } - Dictionary> keyValueTags; - HashSet keyValueTagsToRemove; + public List? Tags { get; private set; } + Dictionary>? keyValueTags; + HashSet? keyValueTagsToRemove; public Editor WithTag( string tag ) { @@ -143,9 +143,10 @@ namespace Steamworks.Ugc return false; } - public async Task SubmitAsync( IProgress progress = null ) + public async Task SubmitAsync( IProgress? progress = null ) { var result = default( PublishResult ); + if (SteamUGC.Internal is null) { return result; } progress?.Report( 0 ); diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 7a69e3819..49ff42292 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -35,22 +35,22 @@ namespace Steamworks.Ugc /// /// The given title of this item /// - public string Title { get; internal set; } + public string? Title { get; internal set; } /// /// The description of this item, in your local language if available /// - public string Description { get; internal set; } + public string? Description { get; internal set; } /// /// A list of tags for this item, all lowercase /// - public string[] Tags { get; internal set; } + public string[]? Tags { get; internal set; } /// /// A dictionary of key value tags for this item, only available from queries WithKeyValueTags(true) /// - public Dictionary KeyValueTags { get; internal set; } + public Dictionary? KeyValueTags { get; internal set; } /// /// App Id of the app that created this item @@ -123,14 +123,14 @@ namespace Steamworks.Ugc public bool IsSubscribed => (State & ItemState.Subscribed) == ItemState.Subscribed; public bool NeedsUpdate => (State & ItemState.NeedsUpdate) == ItemState.NeedsUpdate; - public string Directory + public string? Directory { get { ulong size = 0; uint ts = 0; - if (SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } + if (SteamUGC.Internal != null && SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } return null; } } @@ -147,7 +147,7 @@ namespace Steamworks.Ugc ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) + if ( SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) return (long) total; return -1; @@ -166,7 +166,7 @@ namespace Steamworks.Ugc ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) + if ( SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) return (long)downloaded; return -1; @@ -185,7 +185,7 @@ namespace Steamworks.Ugc ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) return 0; return (long) size; @@ -201,7 +201,7 @@ namespace Steamworks.Ugc { ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) return null; return Epoch.ToDateTime(ts); @@ -226,7 +226,7 @@ namespace Steamworks.Ugc //possibly similar properties should also be changed ulong downloaded = 0; ulong total = 0; - if (SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) + if (SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) { return (float)((double)downloaded / (double)total); } @@ -277,7 +277,7 @@ namespace Steamworks.Ugc /// public bool HasTag( string find ) { - if ( Tags.Length == 0 ) return false; + if ( Tags is null || Tags.Length == 0 ) return false; return Tags.Contains( find, StringComparer.OrdinalIgnoreCase ); } @@ -287,6 +287,7 @@ namespace Steamworks.Ugc /// public async Task Subscribe () { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.SubscribeItem( _id ); return result?.Result == Result.OK; } @@ -296,7 +297,7 @@ namespace Steamworks.Ugc /// If CancellationToken is default then there is 60 seconds timeout /// Progress will be set to 0-1 /// - public async Task DownloadAsync( Action progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) + public async Task DownloadAsync( Action? progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) { return await SteamUGC.DownloadAsync( Id, progress, milisecondsUpdateDelay, ct ); } @@ -305,7 +306,8 @@ namespace Steamworks.Ugc /// Allows the user to unsubscribe from this item /// public async Task Unsubscribe () - { + { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.UnsubscribeItem( _id ); return result?.Result == Result.OK; } @@ -315,6 +317,7 @@ namespace Steamworks.Ugc /// public async Task AddFavorite() { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.AddItemToFavorites(details.ConsumerAppID, _id); return result?.Result == Result.OK; } @@ -324,6 +327,7 @@ namespace Steamworks.Ugc /// public async Task RemoveFavorite() { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.RemoveItemFromFavorites(details.ConsumerAppID, _id); return result?.Result == Result.OK; } @@ -333,6 +337,7 @@ namespace Steamworks.Ugc /// public async Task Vote( bool up ) { + if (SteamUGC.Internal is null) { return null; } var r = await SteamUGC.Internal.SetUserItemVote( Id, up ); return r?.Result; } @@ -342,6 +347,7 @@ namespace Steamworks.Ugc /// public async Task GetUserVote() { + if (SteamUGC.Internal is null) { return null; } var result = await SteamUGC.Internal.GetUserItemVote(_id); if (!result.HasValue) return null; @@ -351,27 +357,27 @@ namespace Steamworks.Ugc /// /// Return a URL to view this item online /// - public string Url => $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}"; + public string Url => $"https://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}"; /// /// The URl to view this item's changelog /// - public string ChangelogUrl => $"http://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}"; + public string ChangelogUrl => $"https://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}"; /// /// The URL to view the comments on this item /// - public string CommentsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/comments/{Id}"; + public string CommentsUrl => $"https://steamcommunity.com/sharedfiles/filedetails/comments/{Id}"; /// /// The URL to discuss this item /// - public string DiscussUrl => $"http://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}"; + public string DiscussUrl => $"https://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}"; /// /// The URL to view this items stats online /// - public string StatsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/stats/{Id}"; + public string StatsUrl => $"https://steamcommunity.com/sharedfiles/filedetails/stats/{Id}"; public ulong NumSubscriptions { get; internal set; } public ulong NumFavorites { get; internal set; } @@ -390,12 +396,12 @@ namespace Steamworks.Ugc /// /// The URL to the preview image for this item /// - public string PreviewImageUrl { get; internal set; } + public string? PreviewImageUrl { get; internal set; } /// /// The metadata string for this item, only available from queries WithMetadata(true) /// - public string Metadata { get; internal set; } + public string? Metadata { get; internal set; } /// /// Edit this item diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index b8bc42740..cfb65ca75 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -14,7 +14,7 @@ namespace Steamworks.Ugc UGCQuery queryType; AppId consumerApp; AppId creatorApp; - string searchText; + string? searchText; public Query( UgcType type ) : this() { @@ -96,7 +96,7 @@ namespace Steamworks.Ugc #endregion #region Files - PublishedFileId[] Files; + PublishedFileId[]? Files; public Query WithFileId( params PublishedFileId[] files ) { @@ -109,6 +109,8 @@ namespace Steamworks.Ugc { if ( page <= 0 ) throw new System.Exception( "page should be > 0" ); + if (SteamUGC.Internal is null) { return null; } + if ( consumerApp == 0 ) consumerApp = SteamClient.AppId; if ( creatorApp == 0 ) creatorApp = consumerApp; @@ -159,16 +161,16 @@ namespace Steamworks.Ugc public QueryType WithType( UgcType type ) { matchingType = type; return this; } int? maxCacheAge; public QueryType AllowCachedResponse( int maxSecondsAge ) { maxCacheAge = maxSecondsAge; return this; } - string language; + string? language; public QueryType InLanguage( string lang ) { language = lang; return this; } int? trendDays; public QueryType WithTrendDays( int days ) { trendDays = days; return this; } - List requiredTags; + List? requiredTags; bool? matchAnyTag; - List excludedTags; - Dictionary requiredKv; + List? excludedTags; + Dictionary? requiredKv; /// /// Found items must have at least one of the defined tags @@ -213,34 +215,34 @@ namespace Steamworks.Ugc if ( requiredTags != null ) { foreach ( var tag in requiredTags ) - SteamUGC.Internal.AddRequiredTag( handle, tag ); + SteamUGC.Internal?.AddRequiredTag( handle, tag ); } if ( excludedTags != null ) { foreach ( var tag in excludedTags ) - SteamUGC.Internal.AddExcludedTag( handle, tag ); + SteamUGC.Internal?.AddExcludedTag( handle, tag ); } if ( requiredKv != null ) { foreach ( var tag in requiredKv ) - SteamUGC.Internal.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); + SteamUGC.Internal?.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); } if ( matchAnyTag.HasValue ) { - SteamUGC.Internal.SetMatchAnyTag( handle, matchAnyTag.Value ); + SteamUGC.Internal?.SetMatchAnyTag( handle, matchAnyTag.Value ); } if ( trendDays.HasValue ) { - SteamUGC.Internal.SetRankedByTrendDays( handle, (uint)trendDays.Value ); + SteamUGC.Internal?.SetRankedByTrendDays( handle, (uint)trendDays.Value ); } if ( !string.IsNullOrEmpty( searchText ) ) { - SteamUGC.Internal.SetSearchText( handle, searchText ); + SteamUGC.Internal?.SetSearchText( handle, searchText ); } } @@ -271,42 +273,42 @@ namespace Steamworks.Ugc { if (WantsReturnOnlyIDs.HasValue) { - SteamUGC.Internal.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value); + SteamUGC.Internal?.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value); } if (WantsReturnKeyValueTags.HasValue) { - SteamUGC.Internal.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value); + SteamUGC.Internal?.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value); } if (WantsReturnLongDescription.HasValue) { - SteamUGC.Internal.SetReturnLongDescription(handle, WantsReturnLongDescription.Value); + SteamUGC.Internal?.SetReturnLongDescription(handle, WantsReturnLongDescription.Value); } if (WantsReturnMetadata.HasValue) { - SteamUGC.Internal.SetReturnMetadata(handle, WantsReturnMetadata.Value); + SteamUGC.Internal?.SetReturnMetadata(handle, WantsReturnMetadata.Value); } if (WantsReturnChildren.HasValue) { - SteamUGC.Internal.SetReturnChildren(handle, WantsReturnChildren.Value); + SteamUGC.Internal?.SetReturnChildren(handle, WantsReturnChildren.Value); } if (WantsReturnAdditionalPreviews.HasValue) { - SteamUGC.Internal.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value); + SteamUGC.Internal?.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value); } if (WantsReturnTotalOnly.HasValue) { - SteamUGC.Internal.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value); + SteamUGC.Internal?.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value); } if (WantsReturnPlaytimeStats.HasValue) { - SteamUGC.Internal.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value); + SteamUGC.Internal?.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs index 86a6e2b51..54ffe6973 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs @@ -24,6 +24,7 @@ namespace Steamworks.Ugc var details = default( SteamUGCDetails_t ); for ( uint i=0; i< ResultCount; i++ ) { + if (SteamUGC.Internal is null) { yield break; } if ( SteamUGC.Internal.GetQueryUGCResult( Handle, i, ref details ) ) { var item = Item.From( details ); @@ -86,7 +87,7 @@ namespace Steamworks.Ugc { ulong val = 0; - if ( !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) ) return 0; return val; @@ -96,7 +97,7 @@ namespace Steamworks.Ugc { if ( Handle > 0 ) { - SteamUGC.Internal.ReleaseQueryUGCRequest( Handle ); + SteamUGC.Internal?.ReleaseQueryUGCRequest( Handle ); Handle = 0; } } diff --git a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs index bc1c4d848..0f35438bd 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -16,13 +16,13 @@ namespace Steamworks private static readonly HashSet ruleResponseHandlers = new HashSet(); - internal static async Task> GetRules(Steamworks.Data.ServerInfo server) + internal static async Task?> GetRules(Steamworks.Data.ServerInfo server) { Status status = Status.Pending; var rules = new Dictionary(); - SteamMatchmakingRulesResponse responseHandler = null; + SteamMatchmakingRulesResponse? responseHandler = null; void onRulesResponded(string key, string value) => rules.Add(key, value); @@ -51,6 +51,8 @@ namespace Steamworks responseHandler = null; } + if (SteamMatchmakingServers.Internal is null) { return null; } + responseHandler = new SteamMatchmakingRulesResponse( onRulesResponded, onRulesFailToRespond, diff --git a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs index a8b13dab4..ca0b5a20c 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs @@ -61,9 +61,9 @@ namespace Steamworks public class SteamSharedClass : SteamClass { - internal static SteamInterface Interface => InterfaceClient ?? InterfaceServer; - internal static SteamInterface InterfaceClient; - internal static SteamInterface InterfaceServer; + internal static SteamInterface? Interface => InterfaceClient ?? InterfaceServer; + internal static SteamInterface? InterfaceClient; + internal static SteamInterface? InterfaceServer; internal override void InitializeInterface( bool server ) { @@ -99,7 +99,7 @@ namespace Steamworks public class SteamClientClass : SteamClass { - internal static SteamInterface Interface; + internal static SteamInterface? Interface; internal override void InitializeInterface( bool server ) { @@ -122,7 +122,7 @@ namespace Steamworks public class SteamServerClass : SteamClass { - internal static SteamInterface Interface; + internal static SteamInterface? Interface; internal override void InitializeInterface( bool server ) { diff --git a/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs b/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs index 1d02c3bf1..59d1517f9 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -48,12 +49,13 @@ namespace Steamworks internal IntPtr ptr; #pragma warning restore 649 - public unsafe static implicit operator string( Utf8StringPointer p ) + [return: NotNullIfNotNull("p")] + public unsafe static implicit operator string?( Utf8StringPointer p ) { - return ConvertPtrToString(p.ptr); + return ConvertPtrToString(p.ptr)!; } - public unsafe static string ConvertPtrToString(IntPtr ptr) + public unsafe static string? ConvertPtrToString(IntPtr ptr) { if (ptr == IntPtr.Zero) return null; diff --git a/Libraries/Facepunch.Steamworks/Utility/Utility.cs b/Libraries/Facepunch.Steamworks/Utility/Utility.cs index 3365d2ff4..ad77d63d2 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Utility.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Utility.cs @@ -10,7 +10,7 @@ namespace Steamworks { public static partial class Utility { - static internal T ToType( this IntPtr ptr ) + static internal T? ToType( this IntPtr ptr ) { if ( ptr == IntPtr.Zero ) return default; @@ -18,7 +18,7 @@ namespace Steamworks return (T)Marshal.PtrToStructure( ptr, typeof( T ) ); } - static internal object ToType( this IntPtr ptr, System.Type t ) + static internal object? ToType( this IntPtr ptr, System.Type t ) { if ( ptr == IntPtr.Zero ) return default; From 5aabc1759f042a7d096efc8ca05eda89171178b7 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 7 Jul 2023 08:58:12 +0300 Subject: [PATCH 04/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 39413a43f..ddcd9a18a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -55,6 +55,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.0.21.0 + - Unstable (v1.1.3.0) - Other validations: required: true From 60b0e2ae3ed5163c823e8d59bc87a26dac4ab8c4 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Mon, 17 Jul 2023 15:22:09 +0300 Subject: [PATCH 05/14] Update bug_report.yml Added a dropdown for selecting whether the issue occurs in sp, mp listen server, mp dedicated server, all of these or none of these. --- .github/ISSUE_TEMPLATE/bug_report.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ddcd9a18a..404d7a0d6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -47,7 +47,26 @@ body: - Happens regularly - Happens every time I play validations: - required: true + required: true + - type: dropdown + id: mporsp + attributes: + label: Single player or multiplayer? + description: Did the issue happen in single player, multiplayer, or both? How was the server being hosted? + options: + - Single player + - Multiplayer hosted from the in-game menu (= using a listen server) + - Multiplayer hosted using a dedicated server + - Happens in both single player and multiplayer + - Happens outside single player or multiplayer game modes (e.g. game launches on startup, something broken in the main menu) + - Other + validations: + required: true + - type: input + id: othermporsp + attributes: + label: "-" + description: If you selected "Other" in the above dropdown, please clarify here. - type: dropdown id: version attributes: From a1407bf009e89873d587135ac9d88fc9dced01a6 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Sat, 19 Aug 2023 12:31:05 +0300 Subject: [PATCH 06/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 404d7a0d6..69e0d97d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.0.21.0 - - Unstable (v1.1.3.0) + - Unstable (v1.1.8.0) - Other validations: required: true From 8376935346b4903902d0a9fa601bde884f35b105 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 25 Aug 2023 14:35:57 +0300 Subject: [PATCH 07/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 69e0d97d2..f70d03d55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.0.21.0 - - Unstable (v1.1.8.0) + - Unstable (v1.1.9.0) - Other validations: required: true From 0294a78afe66ddc22062e14123aa8d083f6215af Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Tue, 29 Aug 2023 18:21:59 +0300 Subject: [PATCH 08/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f70d03d55..bba42f860 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.0.21.0 - - Unstable (v1.1.9.0) + - Unstable (v1.1.10.0) - Other validations: required: true From 639d4d076dae209ba5558af73b810e568a269074 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 22 Sep 2023 17:52:21 +0300 Subject: [PATCH 09/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bba42f860..c327e282c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.0.21.0 - - Unstable (v1.1.10.0) + - Unstable (v1.1.14.0) - Other validations: required: true From cf8f0de6594d0833aaece0ec37feea0f382ac75c Mon Sep 17 00:00:00 2001 From: Markus Isberg Date: Mon, 2 Oct 2023 16:43:54 +0300 Subject: [PATCH 10/14] Unstable 1.1.14.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 4 +- .../ClientSource/Characters/AI/AITarget.cs | 4 +- .../Characters/AI/HumanAIController.cs | 15 +- .../Characters/Animation/Ragdoll.cs | 1 + .../ClientSource/Characters/Character.cs | 30 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 17 +- .../Characters/CharacterNetworking.cs | 37 +- .../Characters/Health/CharacterHealth.cs | 20 +- .../Health/HealingCooldownClient.cs | 2 - .../ClientSource/Characters/Limb.cs | 62 +- .../CircuitBox/CircuitBoxComponent.cs | 114 +++ .../CircuitBox/CircuitBoxConnection.cs | 127 +++ .../CircuitBox/CircuitBoxLabel.cs | 22 + .../CircuitBoxMouseDragSnapshotHandler.cs | 233 ++++++ .../ClientSource/CircuitBox/CircuitBoxNode.cs | 88 ++ .../ClientSource/CircuitBox/CircuitBoxUI.cs | 722 +++++++++++++++++ .../ClientSource/CircuitBox/CircuitBoxWire.cs | 11 + .../CircuitBox/CircuitBoxWireRenderer.cs | 271 +++++++ .../ClientSource/DebugConsole.cs | 120 ++- .../Events/EventActions/ConversationAction.cs | 19 +- .../Events/EventActions/EventLogAction.cs | 11 + .../EventActions/EventObjectiveAction.cs | 66 ++ .../Events/EventActions/MessageBoxAction.cs | 2 +- .../EventActions/TutorialSegmentAction.cs | 43 - .../ClientSource/Events/EventLog.cs | 61 ++ .../ClientSource/Events/EventManager.cs | 49 +- .../Events/Missions/MineralMission.cs | 28 +- .../ClientSource/Events/Missions/Mission.cs | 61 +- .../Events/Missions/NestMission.cs | 18 +- .../ClientSource/Fonts/ScalableFont.cs | 4 +- .../ClientSource/GUI/ChatBox.cs | 13 +- .../ClientSource/GUI/CrewManagement.cs | 4 +- .../ClientSource/GUI/FileSelection.cs | 4 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 33 +- .../ClientSource/GUI/GUIComponent.cs | 2 +- .../ClientSource/GUI/GUIMessage.cs | 6 +- .../ClientSource/GUI/GUIMessageBox.cs | 47 ++ .../ClientSource/GUI/GUIPrefab.cs | 2 +- .../ClientSource/GUI/GUIScissorComponent.cs | 25 +- .../ClientSource/GUI/GUIStyle.cs | 14 +- .../ClientSource/GUI/GUITickBox.cs | 39 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 5 +- .../ClientSource/GUI/LoadingScreen.cs | 86 +- .../ClientSource/GUI/MedicalClinicUI.cs | 2 +- .../ClientSource/GUI/RectTransform.cs | 2 + .../ClientSource/GUI/ShapeExtensions.cs | 26 +- .../ClientSource/GUI/TabMenu.cs | 188 +---- .../ClientSource/GUI/UISprite.cs | 3 + .../GameAnalytics/GameAnalyticsManager.cs | 8 +- .../BarotraumaClient/ClientSource/GameMain.cs | 69 +- .../ClientSource/GameSession/CrewManager.cs | 81 +- .../GameSession/GameModes/CampaignMode.cs | 35 +- .../GameModes/MultiPlayerCampaign.cs | 6 +- .../GameModes/SinglePlayerCampaign.cs | 15 +- .../GameModes/Tutorials/Tutorial.cs | 4 +- .../ClientSource/GameSession/GameSession.cs | 40 +- .../ClientSource/GameSession/HintManager.cs | 49 +- .../GameSession/ObjectiveManager.cs | 91 ++- .../ClientSource/GameSession/ReadyCheck.cs | 72 +- .../ClientSource/GameSession/RoundSummary.cs | 419 ++++++---- .../ClientSource/Items/CharacterInventory.cs | 102 ++- .../ClientSource/Items/Components/Door.cs | 6 +- .../Components/EntitySpawnerComponent.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 14 +- .../Items/Components/Holdable/IdCard.cs | 52 +- .../Items/Components/Holdable/RangedWeapon.cs | 3 +- .../Items/Components/Holdable/Sprayer.cs | 6 +- .../Items/Components/ItemComponent.cs | 48 +- .../Items/Components/ItemContainer.cs | 181 ++++- .../Items/Components/ItemLabel.cs | 2 +- .../ClientSource/Items/Components/Ladder.cs | 5 +- .../Items/Components/LightComponent.cs | 2 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 120 ++- .../Items/Components/Machines/MiniMap.cs | 46 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Machines/Sonar.cs | 70 +- .../Items/Components/Machines/Steering.cs | 30 +- .../ClientSource/Items/Components/Planter.cs | 2 +- .../Items/Components/Power/PowerContainer.cs | 4 +- .../Items/Components/Power/PowerTransfer.cs | 21 +- .../Items/Components/Projectile.cs | 133 +-- .../Items/Components/RemoteController.cs | 2 +- .../Items/Components/RepairTool.cs | 2 +- .../Items/Components/Repairable.cs | 16 +- .../ClientSource/Items/Components/Rope.cs | 54 +- .../Items/Components/Signal/CircuitBox.cs | 499 ++++++++++++ .../Items/Components/Signal/Connection.cs | 235 +++--- .../Components/Signal/ConnectionPanel.cs | 26 +- .../Components/Signal/CustomInterface.cs | 9 +- .../Items/Components/Signal/MotionSensor.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 6 +- .../Items/Components/Signal/WifiComponent.cs | 4 +- .../Items/Components/Signal/Wire.cs | 16 +- .../ClientSource/Items/Components/Turret.cs | 105 +-- .../ClientSource/Items/DockingPort.cs | 14 +- .../ClientSource/Items/Inventory.cs | 141 ++-- .../ClientSource/Items/Item.cs | 112 ++- .../ClientSource/Items/ItemInventory.cs | 9 +- .../ClientSource/Map/Explosion.cs | 97 ++- .../Levels/LevelObjects/LevelObjectManager.cs | 4 +- .../ClientSource/Map/Lights/ConvexHull.cs | 2 +- .../ClientSource/Map/Lights/LightManager.cs | 18 +- .../ClientSource/Map/Lights/LightSource.cs | 108 ++- .../ClientSource/Map/LinkedSubmarine.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 124 +-- .../ClientSource/Map/Structure.cs | 44 +- .../ClientSource/Map/Submarine.cs | 40 +- .../ClientSource/Map/SubmarinePreview.cs | 90 ++- .../Networking/FileTransfer/FileReceiver.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 99 +-- .../Networking/Primitives/Peers/ClientPeer.cs | 10 +- .../Primitives/Peers/LidgrenClientPeer.cs | 15 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 6 +- .../ClientSource/Networking/RespawnManager.cs | 12 +- .../Networking/ServerList/ServerInfo.cs | 11 +- .../ClientSource/Networking/ServerLog.cs | 4 +- .../ClientSource/Networking/ServerSettings.cs | 34 +- .../ClientSource/Networking/Voting.cs | 4 + .../ClientSource/Particles/Particle.cs | 10 +- .../ClientSource/Particles/ParticleEmitter.cs | 28 +- .../ClientSource/Particles/ParticleManager.cs | 30 +- .../ClientSource/Particles/ParticlePrefab.cs | 2 +- .../ClientSource/Physics/PhysicsBody.cs | 4 +- .../BarotraumaClient/ClientSource/Program.cs | 2 +- .../CampaignSetupUI/CampaignSetupUI.cs | 4 +- .../MultiPlayerCampaignSetupUI.cs | 4 +- .../SinglePlayerCampaignSetupUI.cs | 31 +- .../CharacterEditor/CharacterEditorScreen.cs | 16 +- .../Screens/EventEditor/EditorNode.cs | 66 +- ...ection.cs => EventEditorNodeConnection.cs} | 52 +- .../Screens/EventEditor/EventEditorScreen.cs | 22 +- .../ClientSource/Screens/GameScreen.cs | 4 +- .../ClientSource/Screens/LevelEditorScreen.cs | 48 ++ .../ClientSource/Screens/MainMenuScreen.cs | 13 +- .../ClientSource/Screens/ModDownloadScreen.cs | 2 +- .../ClientSource/Screens/NetLobbyScreen.cs | 291 ++++--- .../ServerListScreen/ServerListScreen.cs | 4 +- .../ClientSource/Screens/SubEditorScreen.cs | 190 +++-- .../ClientSource/Screens/TestScreen.cs | 57 +- .../Serialization/SerializableEntityEditor.cs | 11 +- .../ClientSource/Settings/SettingsMenu.cs | 10 +- .../ClientSource/Sounds/OggSound.cs | 190 +++-- .../ClientSource/Sounds/Sound.cs | 22 +- .../ClientSource/Sounds/SoundBuffer.cs | 2 +- .../ClientSource/Sounds/SoundChannel.cs | 86 +- .../ClientSource/Sounds/SoundFilters.cs | 196 ++--- .../ClientSource/Sounds/SoundManager.cs | 4 +- .../ClientSource/Sounds/SoundPlayer.cs | 38 +- .../ClientSource/Sprite/Sprite.cs | 44 + .../ClientSource/Steam/Lobby.cs | 3 +- .../ClientSource/Steam/SteamManager.cs | 2 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 16 +- .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 2 +- .../ClientSource/Traitors/TraitorManager.cs | 22 + .../Traitors/TraitorMissionPrefab.cs | 33 - .../Traitors/TraitorMissionResult.cs | 22 - .../ClientSource/Utils/SpreadsheetExport.cs | 3 +- .../ClientSource/Utils/TextureLoader.cs | 193 ++--- .../ClientSource/Utils/WikiImage.cs | 8 +- .../Content/Effects/wearableclip.xnb | Bin 2416 -> 2416 bytes .../Content/Effects/wearableclip_opengl.xnb | Bin 2380 -> 2380 bytes .../BarotraumaClient/LinuxClient.csproj | 3 +- Barotrauma/BarotraumaClient/MacClient.csproj | 12 +- .../BarotraumaClient/Shaders/wearableclip.fx | 100 +-- .../Shaders/wearableclip_opengl.fx | 100 +-- .../BarotraumaClient/WindowsClient.csproj | 3 +- .../BarotraumaServer/LinuxServer.csproj | 3 +- Barotrauma/BarotraumaServer/MacServer.csproj | 9 +- .../Characters/CharacterNetworking.cs | 29 +- .../CircuitBox/CircuitBoxConnection.cs | 16 + .../ServerSource/DebugConsole.cs | 93 ++- .../Events/EventActions/ConversationAction.cs | 2 +- .../Events/EventActions/EventLogAction.cs | 55 ++ .../EventActions/EventObjectiveAction.cs | 36 + .../ServerSource/Events/EventLog.cs | 22 + .../ServerSource/Events/EventManager.cs | 25 +- .../ServerSource/Events/Missions/Mission.cs | 96 +++ .../Events/Missions/NestMission.cs | 2 - .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../GameModes/CharacterCampaignData.cs | 9 +- .../GameModes/MultiPlayerCampaign.cs | 10 +- .../ServerSource/Items/CharacterInventory.cs | 12 + .../Items/Components/Holdable/Holdable.cs | 1 + .../Items/Components/Projectile.cs | 18 +- .../Items/Components/Repairable.cs | 3 +- .../Items/Components/Signal/CircuitBox.cs | 320 ++++++++ .../Components/Signal/ConnectionPanel.cs | 3 +- .../Items/Components/Signal/Terminal.cs | 12 +- .../ServerSource/Items/Inventory.cs | 63 +- .../ServerSource/Items/Item.cs | 56 +- .../ServerSource/Items/ItemEventData.cs | 19 + .../ServerSource/Items/ItemInventory.cs | 13 + .../BarotraumaServer/ServerSource/Map/Hull.cs | 1 + .../ServerSource/Networking/Client.cs | 2 - .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 115 +-- .../ServerSource/Networking/KarmaManager.cs | 42 +- .../Peers/Server/LidgrenServerPeer.cs | 3 +- .../ServerSource/Networking/RespawnManager.cs | 19 +- .../ServerSource/Networking/ServerSettings.cs | 16 +- .../ServerSource/Networking/Voting.cs | 14 + .../ServerSource/Steam/SteamManager.cs | 5 +- .../ServerSource/Traitors/Goals/Goal.cs | 64 -- .../Traitors/Goals/GoalDestroyItemsWithTag.cs | 107 --- .../Goals/GoalEntityTransformation.cs | 162 ---- .../Traitors/Goals/GoalFindItem.cs | 239 ------ .../Traitors/Goals/GoalFloodPercentOfSub.cs | 46 -- .../Traitors/Goals/GoalInjectTarget.cs | 84 -- .../Goals/GoalKeepTransformedAlive.cs | 73 -- .../Traitors/Goals/GoalKillTarget.cs | 163 ---- .../Goals/GoalReachDistanceFromSub.cs | 56 -- .../Traitors/Goals/GoalReplaceInventory.cs | 70 -- .../Traitors/Goals/GoalSabotageItems.cs | 62 -- .../Traitors/Goals/GoalUnwiring.cs | 96 --- .../Traitors/Goals/GoalWaitForTraitors.cs | 35 - .../Traitors/Goals/HumanoidGoal.cs | 17 - .../Goals/Modifiers/GoalHasDuration.cs | 62 -- .../Goals/Modifiers/GoalHasTimeLimit.cs | 50 -- .../Goals/Modifiers/GoalIsOptional.cs | 36 - .../Traitors/Goals/Modifiers/Modifier.cs | 80 -- .../ServerSource/Traitors/Objective.cs | 194 ----- .../ServerSource/Traitors/Traitor.cs | 43 - .../ServerSource/Traitors/TraitorManager.cs | 604 ++++++++++---- .../ServerSource/Traitors/TraitorMission.cs | 394 --------- .../Traitors/TraitorMissionPrefab.cs | 664 --------------- .../Traitors/TraitorMissionResult.cs | 30 - .../BarotraumaServer/WindowsServer.csproj | 3 +- .../Characters/AI/AIController.cs | 8 +- .../Characters/AI/EnemyAIController.cs | 72 +- .../Characters/AI/HumanAIController.cs | 92 ++- .../Characters/AI/IndoorsSteeringManager.cs | 28 +- .../Characters/AI/NPCConversation.cs | 2 +- .../Characters/AI/Objectives/AIObjective.cs | 42 +- .../Objectives/AIObjectiveChargeBatteries.cs | 1 + .../AI/Objectives/AIObjectiveCleanupItem.cs | 17 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 14 +- .../AI/Objectives/AIObjectiveCombat.cs | 354 ++++---- .../AI/Objectives/AIObjectiveContainItem.cs | 8 +- .../AI/Objectives/AIObjectiveDecontainItem.cs | 11 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 2 +- .../Objectives/AIObjectiveExtinguishFire.cs | 11 +- .../Objectives/AIObjectiveFindDivingGear.cs | 43 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 27 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 36 +- .../AI/Objectives/AIObjectiveFixLeaks.cs | 3 +- .../AI/Objectives/AIObjectiveGetItem.cs | 50 +- .../AI/Objectives/AIObjectiveGetItems.cs | 88 +- .../AI/Objectives/AIObjectiveGoTo.cs | 31 +- .../AI/Objectives/AIObjectiveIdle.cs | 4 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 7 +- .../AI/Objectives/AIObjectiveLoop.cs | 59 +- .../AI/Objectives/AIObjectiveManager.cs | 2 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 6 +- .../AI/Objectives/AIObjectivePrepare.cs | 9 +- .../AI/Objectives/AIObjectivePumpWater.cs | 3 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 84 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 2 +- .../AI/Objectives/AIObjectiveRescue.cs | 133 +-- .../AI/Objectives/AIObjectiveRescueAll.cs | 100 +-- .../AI/Objectives/AIObjectiveReturn.cs | 11 +- .../SharedSource/Characters/AI/Order.cs | 7 + .../SharedSource/Characters/AI/PetBehavior.cs | 97 ++- .../ShipIssueWorkerOperateWeapons.cs | 2 +- .../Characters/AI/ShipCommandManager.cs | 6 +- .../Characters/AI/Wreck/WreckAI.cs | 9 +- .../Characters/AI/Wreck/WreckAIConfig.cs | 2 +- .../Characters/Animation/AnimController.cs | 80 +- .../Animation/HumanoidAnimController.cs | 29 +- .../Characters/Animation/Ragdoll.cs | 53 +- .../SharedSource/Characters/Attack.cs | 23 +- .../SharedSource/Characters/Character.cs | 411 ++++++---- .../Characters/CharacterEventData.cs | 10 +- .../SharedSource/Characters/CharacterInfo.cs | 32 +- .../Characters/CharacterPrefab.cs | 2 + .../Health/Afflictions/AfflictionPrefab.cs | 56 +- .../Characters/Health/CharacterHealth.cs | 34 +- .../SharedSource/Characters/Jobs/Job.cs | 10 +- .../SharedSource/Characters/Limb.cs | 93 ++- .../Characters/Params/CharacterParams.cs | 23 +- .../Characters/Params/EditableParams.cs | 13 +- .../Params/Ragdoll/RagdollParams.cs | 22 +- .../AbilityConditionAttackData.cs | 4 +- .../AbilityConditionMission.cs | 6 +- .../AbilityConditionAllyHasTalent.cs | 22 + .../AbilityConditionHasItem.cs | 8 +- .../AbilityConditionHasStatusTag.cs | 8 +- .../AbilityConditionHasTalent.cs | 3 +- .../AbilityConditionHoldingItem.cs | 4 +- .../CharacterAbilityGiveItemStatToTags.cs | 5 +- .../CharacterAbilityGivePermanentStat.cs | 4 +- .../CharacterAbilityByTheBook.cs | 4 +- .../CharacterAbilityRegenerateLoot.cs | 35 +- .../CharacterAbilityTandemFire.cs | 6 +- .../Characters/Talents/TalentTree.cs | 11 +- .../CircuitBox/CircuitBoxComponent.cs | 84 ++ .../CircuitBox/CircuitBoxConnection.cs | 121 +++ .../CircuitBox/CircuitBoxCursor.cs | 109 +++ .../CircuitBox/CircuitBoxInputOutputNode.cs | 38 + .../CircuitBox/CircuitBoxNetStructs.cs | 163 ++++ .../SharedSource/CircuitBox/CircuitBoxNode.cs | 132 +++ .../CircuitBox/CircuitBoxSelectable.cs | 43 + .../CircuitBox/CircuitBoxSizes.cs | 17 + .../SharedSource/CircuitBox/CircuitBoxWire.cs | 186 +++++ .../CircuitBox/ICircuitBoxIdentifiable.cs | 28 + .../CircuitBox/ItemSlotIndexPair.cs | 44 + .../ContentFile/ContentFile.cs | 16 + .../ContentFile/RandomEventsFile.cs | 12 +- .../ContentFile/SubmarineFile.cs | 4 + .../ContentFile/TraitorMissionsFile.cs | 26 - .../ContentPackage/ContentPackage.cs | 4 +- .../ContentPackageManager.cs | 6 + .../ContentManagement/ContentPath.cs | 24 +- .../ContentManagement/ContentXElement.cs | 3 +- .../SharedSource/CoroutineManager.cs | 188 ++--- .../SharedSource/DebugConsole.cs | 81 +- .../SharedSource/Events/ArtifactEvent.cs | 13 +- .../SharedSource/Events/Event.cs | 5 + .../EventActions/CheckAfflictionAction.cs | 12 +- .../EventActions/CheckConditionalAction.cs | 2 +- .../Events/EventActions/CheckDataAction.cs | 27 +- .../Events/EventActions/CheckItemAction.cs | 135 +++- .../CheckTraitorEventStateAction.cs | 35 + .../EventActions/CheckTraitorVoteAction.cs | 40 + .../EventActions/CheckVisibilityAction.cs | 60 ++ .../Events/EventActions/CombatAction.cs | 7 +- .../Events/EventActions/ConversationAction.cs | 42 +- .../Events/EventActions/CountTargetsAction.cs | 120 +++ .../Events/EventActions/EventAction.cs | 10 +- .../Events/EventActions/EventLogAction.cs | 94 +++ ...gmentAction.cs => EventObjectiveAction.cs} | 21 +- .../Events/EventActions/GiveExpAction.cs | 49 ++ .../Events/EventActions/GiveSkillExpAction.cs | 4 +- .../SharedSource/Events/EventActions/GoTo.cs | 2 - .../EventActions/NPCChangeTeamAction.cs | 2 +- .../Events/EventActions/NPCFollowAction.cs | 7 +- .../EventActions/NPCOperateItemAction.cs | 1 + .../Events/EventActions/NPCWaitAction.cs | 1 + .../Events/EventActions/OnRoundEndAction.cs | 42 + .../SetTraitorEventStateAction.cs | 48 ++ .../Events/EventActions/SpawnAction.cs | 166 +++- .../Events/EventActions/TagAction.cs | 169 +++- .../Events/EventActions/TriggerAction.cs | 47 +- .../EventActions/TutorialHighlightAction.cs | 8 +- .../WaitForItemFabricatedAction.cs | 75 ++ .../EventActions/WaitForItemUsedAction.cs | 153 ++++ .../SharedSource/Events/EventLog.cs | 65 ++ .../SharedSource/Events/EventManager.cs | 125 +-- .../SharedSource/Events/EventPrefab.cs | 26 +- .../SharedSource/Events/EventSet.cs | 6 +- .../Missions/AbandonedOutpostMission.cs | 6 +- .../Events/Missions/BeaconMission.cs | 7 +- .../Events/Missions/CargoMission.cs | 6 + .../Events/Missions/EndMission.cs | 15 +- .../Events/Missions/EscortMission.cs | 12 +- .../Events/Missions/MineralMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 95 +-- .../Events/Missions/MissionPrefab.cs | 32 +- .../Events/Missions/MonsterMission.cs | 2 +- .../Events/Missions/NestMission.cs | 49 +- .../Events/Missions/PirateMission.cs | 2 +- .../Events/Missions/SalvageMission.cs | 22 +- .../Events/Missions/ScanMission.cs | 36 +- .../SharedSource/Events/MonsterEvent.cs | 99 ++- .../SharedSource/Events/ScriptedEvent.cs | 177 +++- .../Extensions/ColorExtensions.cs | 30 + .../Extensions/IEnumerableExtensions.cs | 22 +- .../Extensions/StringExtensions.cs | 4 +- .../GameAnalytics/GameAnalyticsConsent.cs | 198 +++-- .../GameSession/AutoItemPlacer.cs | 19 +- .../SharedSource/GameSession/CargoManager.cs | 12 +- .../SharedSource/GameSession/CrewManager.cs | 9 +- .../SharedSource/GameSession/Data/Factions.cs | 3 + .../GameSession/GameModes/CampaignMode.cs | 36 +- .../GameModes/MultiPlayerCampaign.cs | 3 + .../SharedSource/GameSession/GameSession.cs | 74 +- .../SharedSource/Items/CharacterInventory.cs | 23 +- .../Items/Components/DockingPort.cs | 4 +- .../SharedSource/Items/Components/Door.cs | 23 +- .../Items/Components/ElectricalDischarger.cs | 87 +- .../SharedSource/Items/Components/Growable.cs | 8 +- .../Items/Components/Holdable/Holdable.cs | 57 +- .../Items/Components/Holdable/IdCard.cs | 18 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/Holdable/MeleeWeapon.cs | 9 +- .../Items/Components/Holdable/Pickable.cs | 48 +- .../Items/Components/Holdable/RepairTool.cs | 59 +- .../Items/Components/ItemComponent.cs | 48 +- .../Items/Components/ItemContainer.cs | 152 +++- .../Items/Components/Machines/Controller.cs | 73 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 12 +- .../Items/Components/Machines/Fabricator.cs | 187 +++-- .../Items/Components/Machines/MiniMap.cs | 2 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 11 +- .../Items/Components/Machines/Steering.cs | 6 +- .../Items/Components/Power/PowerContainer.cs | 39 +- .../Items/Components/Power/PowerTransfer.cs | 2 +- .../Items/Components/Power/Powered.cs | 28 +- .../Items/Components/Projectile.cs | 69 +- .../Items/Components/RemoteController.cs | 3 +- .../Items/Components/Repairable.cs | 58 +- .../SharedSource/Items/Components/Rope.cs | 37 +- .../BooleanOperatorComponent.cs | 2 +- .../Items/Components/Signal/CircuitBox.cs | 684 ++++++++++++++++ .../Items/Components/Signal/Connection.cs | 54 +- .../Components/Signal/ConnectionPanel.cs | 18 +- .../Components/Signal/CustomInterface.cs | 7 +- .../Components/Signal/EqualsComponent.cs | 2 +- .../Items/Components/Signal/LightComponent.cs | 16 + .../Items/Components/Signal/MotionSensor.cs | 75 +- .../Components/Signal/RegExFindComponent.cs | 13 +- .../Items/Components/Signal/RelayComponent.cs | 4 +- .../Items/Components/Signal/Terminal.cs | 24 +- .../Items/Components/Signal/WaterDetector.cs | 7 +- .../Items/Components/Signal/WifiComponent.cs | 35 +- .../Items/Components/Signal/Wire.cs | 36 +- .../SharedSource/Items/Components/Turret.cs | 169 ++-- .../SharedSource/Items/Components/Wearable.cs | 59 +- .../SharedSource/Items/Inventory.cs | 198 +++-- .../SharedSource/Items/Item.cs | 403 ++++++++-- .../SharedSource/Items/ItemEventData.cs | 22 +- .../SharedSource/Items/ItemInventory.cs | 16 +- .../SharedSource/Items/ItemPrefab.cs | 131 ++- .../SharedSource/Items/RelatedItem.cs | 41 +- .../SharedSource/Items/StartItemSet.cs | 6 +- .../SharedSource/Map/Explosion.cs | 67 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 8 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 74 +- .../SharedSource/Map/Levels/Level.cs | 228 ++++-- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../Map/Levels/LevelGenerationParams.cs | 12 +- .../Levels/LevelObjects/LevelObjectManager.cs | 19 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 7 + .../Map/Levels/Ruins/RuinGenerationParams.cs | 3 + .../Map/Levels/Ruins/RuinGenerator.cs | 2 +- .../SharedSource/Map/LinkedSubmarine.cs | 3 +- .../SharedSource/Map/Map/Location.cs | 9 +- .../SharedSource/Map/Map/LocationType.cs | 3 + .../SharedSource/Map/Map/Map.cs | 55 +- .../SharedSource/Map/Map/Radiation.cs | 16 +- .../SharedSource/Map/MapEntity.cs | 96 ++- .../SharedSource/Map/MapEntityPrefab.cs | 24 +- .../Map/Outposts/OutpostGenerationParams.cs | 6 + .../Map/Outposts/OutpostGenerator.cs | 194 +++-- .../SharedSource/Map/Structure.cs | 60 +- .../SharedSource/Map/Submarine.cs | 192 +++-- .../SharedSource/Map/SubmarineBody.cs | 26 +- .../SharedSource/Map/WayPoint.cs | 2 +- .../SharedSource/Networking/ChatMessage.cs | 8 +- .../SharedSource/Networking/Client.cs | 8 +- .../SharedSource/Networking/EntitySpawner.cs | 93 ++- .../SharedSource/Networking/NetworkMember.cs | 15 +- .../Primitives/NetworkExtensions.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 5 +- .../SharedSource/Networking/ServerLog.cs | 3 + .../SharedSource/Networking/ServerSettings.cs | 73 +- .../SharedSource/Networking/Voting.cs | 8 +- .../SharedSource/Physics/PhysicsBody.cs | 18 +- .../SharedSource/Screens/GameScreen.cs | 17 +- .../SharedSource/Screens/NetLobbyScreen.cs | 43 +- .../Serialization/SerializableProperty.cs | 60 +- .../Serialization/XMLExtensions.cs | 36 +- .../SharedSource/Settings/GameSettings.cs | 8 +- .../SharedSource/Sprite/ConditionalSprite.cs | 3 +- .../SharedSource/Sprite/Sprite.cs | 54 +- .../StatusEffects/PropertyConditional.cs | 746 ++++++++++------- .../StatusEffects/StatusEffect.cs | 600 ++++++++------ .../SharedSource/Steam/AuthTicket.cs | 63 +- .../SharedSource/Steam/Workshop.cs | 11 +- .../SharedSource/SteamAchievementManager.cs | 2 +- .../BarotraumaShared/SharedSource/Tags.cs | 102 +++ .../SharedSource/Text/TextManager.cs | 34 +- .../SharedSource/Traitors/TraitorEvent.cs | 126 +++ .../Traitors/TraitorEventPrefab.cs | 350 ++++++++ .../SharedSource/Traitors/TraitorManager.cs | 49 ++ .../Traitors/TraitorMissionResult.cs | 15 - .../SharedSource/Utils/CrossThread.cs | 2 +- .../SharedSource/Utils/Md5Hash.cs | 11 + .../SharedSource/Utils/Option/Option.cs | 13 + .../SharedSource/Utils/SaveUtil.cs | 318 ++++---- .../SharedSource/Utils/ToolBox.cs | 71 +- Barotrauma/BarotraumaShared/changelog.txt | 289 ++++++- Barotrauma/BarotraumaShared/hintmanager.xml | 15 +- .../BarotraumaShared/libsteam_api.dylib | Bin 446352 -> 0 bytes .../BarotraumaShared/libsteam_api64.dylib | Bin 446352 -> 0 bytes Barotrauma/BarotraumaShared/libsteam_api64.so | Bin 398662 -> 0 bytes .../Callbacks/CallResult.cs | 5 +- .../Classes/AuthTicketForWebApi.cs | 38 + .../Facepunch.Steamworks.Posix.csproj | 11 +- .../Facepunch.Steamworks.Win64.csproj | 16 +- .../Generated/CustomEnums.cs | 52 +- .../Generated/Interfaces/ISteamAppList.cs | 8 +- .../Generated/Interfaces/ISteamApps.cs | 41 +- .../Generated/Interfaces/ISteamClient.cs | 2 +- .../Generated/Interfaces/ISteamController.cs | 8 +- .../Generated/Interfaces/ISteamFriends.cs | 69 +- .../Generated/Interfaces/ISteamGameSearch.cs | 5 +- .../Generated/Interfaces/ISteamGameServer.cs | 132 ++- .../Interfaces/ISteamGameServerStats.cs | 2 +- .../Generated/Interfaces/ISteamHTMLSurface.cs | 2 +- .../Generated/Interfaces/ISteamHTTP.cs | 2 +- .../Generated/Interfaces/ISteamInput.cs | 167 +++- .../Generated/Interfaces/ISteamInventory.cs | 56 +- .../Generated/Interfaces/ISteamMatchmaking.cs | 8 +- .../ISteamMatchmakingPingResponse.cs | 2 +- .../ISteamMatchmakingPlayersResponse.cs | 2 +- .../ISteamMatchmakingRulesResponse.cs | 2 +- .../ISteamMatchmakingServerListResponse.cs | 2 +- .../Interfaces/ISteamMatchmakingServers.cs | 2 +- .../Generated/Interfaces/ISteamMusic.cs | 2 +- .../Generated/Interfaces/ISteamMusicRemote.cs | 2 +- .../Generated/Interfaces/ISteamNetworking.cs | 2 +- ...teamNetworkingConnectionCustomSignaling.cs | 41 - ...eamNetworkingCustomSignalingRecvContext.cs | 40 - .../Interfaces/ISteamNetworkingFakeUDPPort.cs | 61 ++ .../Interfaces/ISteamNetworkingMessages.cs | 96 +++ .../Interfaces/ISteamNetworkingSockets.cs | 157 +++- .../Interfaces/ISteamNetworkingUtils.cs | 167 +++- .../Interfaces/ISteamParentalSettings.cs | 2 +- .../Generated/Interfaces/ISteamParties.cs | 8 +- .../Generated/Interfaces/ISteamRemotePlay.cs | 2 +- .../Interfaces/ISteamRemoteStorage.cs | 54 +- .../Generated/Interfaces/ISteamScreenshots.cs | 2 +- .../Generated/Interfaces/ISteamTV.cs | 97 --- .../Generated/Interfaces/ISteamUGC.cs | 159 +++- .../Generated/Interfaces/ISteamUser.cs | 56 +- .../Generated/Interfaces/ISteamUserStats.cs | 41 +- .../Generated/Interfaces/ISteamUtils.cs | 90 ++- .../Generated/Interfaces/ISteamVideo.cs | 5 +- .../Generated/SteamCallbacks.cs | 375 ++++++--- .../Generated/SteamConstants.cs | 38 +- .../Generated/SteamEnums.cs | 755 +++++++++++++----- .../Generated/SteamStructFunctions.cs | 51 ++ .../Generated/SteamStructs.cs | 72 +- .../Generated/SteamTypes.cs | 192 ----- .../Networking/BroadcastBufferManager.cs | 282 +++++++ .../Networking/Connection.cs | 65 +- .../Networking/ConnectionLaneStatus.cs | 34 + .../Networking/ConnectionManager.cs | 207 ++++- .../Networking/ConnectionStatus.cs | 77 ++ .../Networking/Delegates.cs | 28 + .../Networking/ISocketManager.cs | 2 +- .../Networking/NetAddress.cs | 12 + .../Networking/NetDebugFunc.cs | 9 - .../Networking/NetKeyValue.cs | 2 +- .../Facepunch.Steamworks/Networking/NetMsg.cs | 9 +- .../Networking/SocketManager.cs | 45 +- Libraries/Facepunch.Steamworks/SteamApps.cs | 109 ++- Libraries/Facepunch.Steamworks/SteamClient.cs | 42 +- .../Facepunch.Steamworks/SteamFriends.cs | 136 +++- Libraries/Facepunch.Steamworks/SteamInput.cs | 56 +- .../Facepunch.Steamworks/SteamInventory.cs | 20 +- .../Facepunch.Steamworks/SteamMatchmaking.cs | 41 +- .../SteamMatchmakingServers.cs | 9 +- Libraries/Facepunch.Steamworks/SteamMusic.cs | 27 +- .../Facepunch.Steamworks/SteamNetworking.cs | 35 +- .../SteamNetworkingSockets.cs | 158 +++- .../SteamNetworkingUtils.cs | 186 ++++- .../Facepunch.Steamworks/SteamParental.cs | 8 +- .../Facepunch.Steamworks/SteamParties.cs | 19 +- .../Facepunch.Steamworks/SteamRemotePlay.cs | 17 +- .../SteamRemoteStorage.cs | 40 +- .../Facepunch.Steamworks/SteamScreenshots.cs | 26 +- Libraries/Facepunch.Steamworks/SteamServer.cs | 70 +- .../Facepunch.Steamworks/SteamServerStats.cs | 49 +- Libraries/Facepunch.Steamworks/SteamUgc.cs | 111 ++- Libraries/Facepunch.Steamworks/SteamUser.cs | 95 ++- .../Facepunch.Steamworks/SteamUserStats.cs | 42 +- Libraries/Facepunch.Steamworks/SteamUtils.cs | 102 ++- Libraries/Facepunch.Steamworks/SteamVideo.cs | 19 +- .../Structs/Achievement.cs | 29 +- .../Facepunch.Steamworks/Structs/AppId.cs | 5 +- .../Structs/DlcInformation.cs | 16 +- .../Structs/DownloadProgress.cs | 21 +- .../Structs/FileDetails.cs | 8 +- .../Facepunch.Steamworks/Structs/Friend.cs | 2 +- .../Facepunch.Steamworks/Structs/Image.cs | 20 +- .../Facepunch.Steamworks/Structs/Lobby.cs | 88 +- .../Structs/PartyBeacon.cs | 12 +- .../Structs/Screenshot.cs | 10 +- .../Facepunch.Steamworks/Structs/Server.cs | 2 +- .../Structs/ServerInit.cs | 13 +- .../Facepunch.Steamworks/Structs/Stat.cs | 2 +- .../Facepunch.Steamworks/Structs/SteamId.cs | 5 +- .../Structs/UgcAdditionalPreview.cs | 22 + .../Structs/UgcAdditionalPreview.cs.meta | 11 + .../Facepunch.Steamworks/Structs/UgcEditor.cs | 23 +- .../Facepunch.Steamworks/Structs/UgcItem.cs | 25 +- .../Facepunch.Steamworks/Structs/UgcQuery.cs | 2 + .../Structs/UgcResultPage.cs | 34 +- .../Facepunch.Steamworks/Utility/Helpers.cs | 39 +- .../Utility/SteamInterface.cs | 14 +- .../Facepunch.Steamworks/libsteam_api64.dylib | Bin 0 -> 610496 bytes .../Facepunch.Steamworks/libsteam_api64.so | Bin 0 -> 391056 bytes .../Facepunch.Steamworks/steam_api64.dll | Bin 262944 -> 298856 bytes .../Dynamics/Contacts/Contact.cs | 6 +- Libraries/Lidgren.Network/NetPeer.Internal.cs | 404 +++++----- .../NetPeer.LatencySimulation.cs | 40 +- Libraries/Lidgren.Network/NetPeer.cs | 21 +- .../Lidgren.Network/NetPeerConfiguration.cs | 53 +- Libraries/Lidgren.Network/NetUtility.cs | 82 +- 606 files changed, 21906 insertions(+), 11456 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs rename Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/{NodeConnection.cs => EventEditorNodeConnection.cs} (86%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs rename Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/{TutorialSegmentAction.cs => EventObjectiveAction.cs} (62%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Tags.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs delete mode 100644 Barotrauma/BarotraumaShared/libsteam_api.dylib delete mode 100644 Barotrauma/BarotraumaShared/libsteam_api64.dylib delete mode 100644 Barotrauma/BarotraumaShared/libsteam_api64.so create mode 100644 Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/Delegates.cs delete mode 100644 Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta create mode 100644 Libraries/Facepunch.Steamworks/libsteam_api64.dylib create mode 100644 Libraries/Facepunch.Steamworks/libsteam_api64.so diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 187b87f7e..272aace14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -255,7 +255,7 @@ namespace Barotrauma /// public bool Freeze { get; set; } - public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true, bool? followSub = null) + public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true, bool allowInput = true, bool? followSub = null) { prevPosition = position; prevZoom = zoom; @@ -268,7 +268,7 @@ namespace Barotrauma Vector2 moveInput = Vector2.Zero; if (allowMove && !Freeze) { - if (GUI.KeyboardDispatcher.Subscriber == null) + if (GUI.KeyboardDispatcher.Subscriber == null && allowInput) { if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index 51bf3a451..dd4b2855e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -50,9 +50,9 @@ namespace Barotrauma } else if (Entity is Item i) { - if (i.Submarine != null && i.GetComponent() == null) + if (i.Submarine != null && i.Container != null) { - // Don't show items that are inside the submarine, because monsters shouldn't target them when they are inside and the monsters are outside. + // Don't show contained items that are inside the submarine, because they shouldn't attract monsters. return; } color = Color.CadetBlue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 3c2985c11..672fffbb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -82,15 +82,20 @@ namespace Barotrauma { var previousNode = path.Nodes[i - 1]; var currentNode = path.Nodes[i]; + bool isPathActive = !path.Finished && !path.IsAtEndNode; + Color pathColor = isPathActive ? Color.Blue * 0.5f : Color.Gray; GUI.DrawLine(spriteBatch, new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y), new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), - Color.Blue * 0.5f, 0, 3); + pathColor, 0, 3); - GUIStyle.SmallFont.DrawString(spriteBatch, - currentNode.ID.ToString(), - new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), - Color.Blue); + if (isPathActive) + { + GUIStyle.SmallFont.DrawString(spriteBatch, + currentNode.ID.ToString(), + new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), + Color.Blue); + } } if (path.CurrentNode != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 6f3eda991..c09d92817 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -469,6 +469,7 @@ namespace Barotrauma float gibParticleAmount = MathHelper.Clamp(limb.Mass / character.AnimController.Mass, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.GibEmitters) { + if (emitter?.Prefab == null) { continue; } if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 5c54b9ba2..115a95e77 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -23,8 +23,6 @@ namespace Barotrauma protected float hudInfoTimer = 1.0f; protected bool hudInfoVisible = false; - private float pressureParticleTimer; - private float findFocusedTimer; protected float lastRecvPositionUpdateTime; @@ -299,7 +297,9 @@ namespace Barotrauma keys[i].SetState(); } - if (CharacterInventory.IsMouseOnInventory && CharacterHUD.ShouldDrawInventory(this)) + if (CharacterInventory.IsMouseOnInventory && + !keys[(int)InputType.Aim].Held && + CharacterHUD.ShouldDrawInventory(this)) { ResetInputIfPrimaryMouse(InputType.Use); ResetInputIfPrimaryMouse(InputType.Shoot); @@ -340,13 +340,6 @@ namespace Barotrauma cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); - - pressureParticleTimer += pressure * deltaTime; - if (pressureParticleTimer > 10.0f) - { - GameMain.ParticleManager.CreateParticle(Params.BleedParticleWater, WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); - pressureParticleTimer = 0.0f; - } } } @@ -722,7 +715,8 @@ namespace Barotrauma break; default: var petBehavior = enemyAI.PetBehavior; - if (petBehavior != null && petBehavior.Happiness < petBehavior.MaxHappiness * 0.25f) + if (petBehavior != null && + (petBehavior.Happiness < petBehavior.UnhappyThreshold || petBehavior.Hunger > petBehavior.HungryThreshold)) { PlaySound(CharacterSound.SoundType.Unhappy); } @@ -859,21 +853,11 @@ namespace Barotrauma if (Controlled.AnimController.Stairs != null) { + //consider the bottom of the stairs the "floor of the room the controlled character is in" yPos = Controlled.AnimController.Stairs.SimPosition.Y - Controlled.AnimController.Stairs.RectHeight * 0.5f; } - foreach (var ladder in Ladder.List) - { - if (CanInteractWith(ladder.Item) && Controlled.CanInteractWith(ladder.Item)) - { - float xPos = ladder.Item.SimPosition.X; - if (Math.Abs(xPos - SimPosition.X) < 3.0) - { - yPos = ladder.Item.SimPosition.Y - ladder.Item.RectHeight * 0.5f; - } - break; - } - } + //don't show the HUD texts if the character is below the floor of the room the controlled character is in if (AnimController.FloorY < yPos) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 83c669f6e..5f0c45241 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -99,7 +99,7 @@ namespace Barotrauma public override bool Interrupted => Character.Removed || !Character.Enabled; public override Color Color => - Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.ParalysisType) > 0 ? + Character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.ParalysisType) > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; public override string NumberToDisplay => string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 47d87e1ca..dce0ec9cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -19,7 +19,6 @@ namespace Barotrauma public bool LastControlled; public int CrewListIndex { get; set; } = -1; - #warning TODO: Refactor private Sprite disguisedPortrait; private List disguisedAttachmentSprites; private Vector2? disguisedSheetIndex; @@ -609,7 +608,6 @@ namespace Barotrauma CharacterInfo = info; parentComponent = parent; HasIcon = hasIcon; - RecreateFrameContents(); } @@ -848,6 +846,12 @@ namespace Barotrauma var info = CharacterInfo; + if (info.HeadSprite == null) + { + DebugConsole.ThrowError($"Head Selection: the head sprite is null! Failed to open the head selection."); + return false; + } + float characterHeightWidthRatio = info.HeadSprite.size.Y / info.HeadSprite.size.X; HeadSelectionList ??= new GUIListBox( new RectTransform( @@ -885,8 +889,13 @@ namespace Barotrauma GUILayoutGroup row = null; int itemsInRow = 0; - ContentXElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => + ContentXElement headElement = info.Ragdoll.MainElement?.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); + if (headElement == null) + { + DebugConsole.ThrowError($"Head Selection: the head element is null in {info.ragdoll.FileName}! Failed to open the head selection."); + return false; + } ContentXElement headSpriteElement = headElement.GetChildElement("sprite"); ContentPath spritePathWithTags = headSpriteElement.GetAttributeContentPath("texture"); @@ -963,7 +972,7 @@ namespace Barotrauma private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) { var info = CharacterInfo; - int index = (int)scrollBar.BarScrollValue; + int index = (int)Math.Round(scrollBar.BarScrollValue); switch (type) { case WearableType.Beard: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8cd302262..de19d3561 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -201,9 +201,9 @@ namespace Barotrauma keys[(int)InputType.Use].Held = useInput; keys[(int)InputType.Use].SetState(false, useInput); + bool crouching = msg.ReadBoolean(); if (AnimController is HumanoidAnimController) { - bool crouching = msg.ReadBoolean(); keys[(int)InputType.Crouch].Held = crouching; keys[(int)InputType.Crouch].SetState(false, crouching); } @@ -269,7 +269,34 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); - AIController?.ClientRead(msg); + bool isEnemyAi = msg.ReadBoolean(); + if (isEnemyAi) + { + byte aiState = msg.ReadByte(); + if (AIController is EnemyAIController enemyAi) + { + enemyAi.State = (AIState)aiState; + } + else + { + DebugConsole.AddWarning($"Received enemy AI data for a character with no {nameof(EnemyAIController)}. Ignoring..."); + } + bool isPet = msg.ReadBoolean(); + if (isPet) + { + byte happiness = msg.ReadByte(); + byte hunger = msg.ReadByte(); + if ((AIController as EnemyAIController)?.PetBehavior is PetBehavior petBehavior) + { + petBehavior.Happiness = (float)happiness / byte.MaxValue * petBehavior.MaxHappiness; + petBehavior.Hunger = (float)hunger / byte.MaxValue * petBehavior.MaxHunger; + } + else + { + DebugConsole.AddWarning($"Received pet AI data for a character with no {nameof(PetBehavior)}. Ignoring..."); + } + } + } } msg.ReadPadBits(); @@ -323,7 +350,7 @@ namespace Barotrauma } else { - Inventory.ClientEventRead(msg, sendingTime); + Inventory.ClientEventRead(msg); } break; case EventType.Control: @@ -379,7 +406,7 @@ namespace Barotrauma float targetY = msg.ReadSingle(); Vector2 targetSimPos = new Vector2(targetX, targetY); //255 = entity already removed, no need to do anything - if (attackLimbIndex == 255 || Removed) { break; } + if (attackLimbIndex == 255 || targetEntityID == NullEntityID || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { //it's possible to get these errors when mid-round syncing, as the client may not @@ -639,7 +666,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found."); + DebugConsole.AddSafeError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found."); } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 602518293..76919ce12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -257,7 +257,8 @@ namespace Barotrauma { for (int i = 0; i < character.Inventory.Capacity; i++) { - if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface || Character.Controlled != Character) { continue; } + if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; } + if (character.Inventory.HideSlot(i)) { continue; } //don't draw the item if it's being dragged out of the slot bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); @@ -479,6 +480,17 @@ namespace Barotrauma inventoryScale = Inventory.UIScale; uiScale = GUI.Scale; + showHiddenAfflictionsButton.RectTransform.NonScaledSize = new Point(afflictionIconContainer.Rect.Height); + //remove affliction icons so we recreate and resize them + for (int i = afflictionIconContainer.CountChildren - 1; i >= 0; i--) + { + var child = afflictionIconContainer.GetChild(i); + if (child.UserData is AfflictionPrefab) + { + afflictionIconContainer.RemoveChild(child); + } + } + healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; @@ -496,6 +508,8 @@ namespace Barotrauma } healthWindow.RectTransform.RecalculateChildren(false); + + Character.Inventory?.RefreshSlotPositions(); } public void UpdateClientSpecific(float deltaTime) @@ -579,7 +593,7 @@ namespace Barotrauma bool inWater = Character.AnimController.InWater; var drawTarget = inWater ? Particles.ParticlePrefab.DrawTargetType.Water : Particles.ParticlePrefab.DrawTargetType.Air; - var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab.DrawTarget == drawTarget || e.Prefab.ParticlePrefab.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); + var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab?.DrawTarget == drawTarget || e.Prefab.ParticlePrefab?.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); float particleMinScale = emitter?.Prefab.Properties.ScaleMin ?? 0.5f; float particleMaxScale = emitter?.Prefab.Properties.ScaleMax ?? 1; float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); @@ -2129,7 +2143,7 @@ namespace Barotrauma { var affliction = kvp.Key; float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; - if (kvp.Value == limbHealths[limb.HealthIndex]) + if (kvp.Value == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific) { limb.BurnOverlayStrength += burnStrength; limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs index bcc7d4083..49623c6a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs @@ -12,8 +12,6 @@ namespace Barotrauma private static DateTimeOffset OnCooldownUntil = DateTimeOffset.MinValue; private const float CooldownDuration = 0.5f; - public static readonly Identifier MedicalItemTag = new Identifier("medical"); - public static void PutOnCooldown() { OnCooldownUntil = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 61729cd15..f6f7a7331 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -432,20 +432,20 @@ namespace Barotrauma { if (Sprite != null) { - Sprite.Remove(); var source = Sprite.SourceElement; + Sprite.Remove(); Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams, ref _texturePath)); } if (_deformSprite != null) { - _deformSprite.Remove(); var source = _deformSprite.Sprite.SourceElement; + _deformSprite.Remove(); _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams, ref _texturePath)); } if (DamagedSprite != null) { - DamagedSprite.Remove(); var source = DamagedSprite.SourceElement; + DamagedSprite.Remove(); DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams, ref _damagedTexturePath)); } for (int i = 0; i < ConditionalSprites.Count; i++) @@ -458,8 +458,8 @@ namespace Barotrauma for (int i = 0; i < DecorativeSprites.Count; i++) { var decorativeSprite = DecorativeSprites[i]; - decorativeSprite.Remove(); var source = decorativeSprite.Sprite.SourceElement; + decorativeSprite.Remove(); DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i], ref _texturePath)); } } @@ -478,9 +478,18 @@ namespace Barotrauma { if (spriteParams != null) { - ContentPath texturePath = - character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) - ?? ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); + //1. check if the variant file redefines the texture + ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage); + //2. check if the base prefab defines the texture + if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty) + { + RagdollParams parentRagdollParams = character.IsHumanoid ? + RagdollParams.GetRagdollParams(character.Prefab.VariantOf) : + RagdollParams.GetRagdollParams(character.Prefab.VariantOf); + texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture"); + } + //3. "default case", get the texture from this character's XML + texturePath ??= ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); path = GetSpritePath(texturePath); } else @@ -592,6 +601,7 @@ namespace Barotrauma { foreach (ParticleEmitter emitter in character.DamageEmitters) { + if (emitter?.Prefab == null) { continue; } if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } ParticlePrefab overrideParticle = null; @@ -614,6 +624,7 @@ namespace Barotrauma foreach (ParticleEmitter emitter in character.BloodEmitters) { + if (emitter?.Prefab == null) { continue; } if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); @@ -727,7 +738,7 @@ namespace Barotrauma } } - float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); + float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || @@ -890,6 +901,28 @@ namespace Barotrauma foreach (WearableSprite wearable in WearingItems) { if (onlyDrawable != null && onlyDrawable != wearable && wearable.CanBeHiddenByOtherWearables) { continue; } + if (wearable.CanBeHiddenByItem.Any()) + { + bool hiddenByOtherItem = false; + foreach (var otherWearable in WearingItems) + { + if (otherWearable == wearable) { continue; } + if (wearable.CanBeHiddenByItem.Contains(otherWearable.WearableComponent.Item.Prefab.Identifier)) + { + hiddenByOtherItem = true; + break; + } + foreach (Identifier tag in wearable.CanBeHiddenByItem) + { + if (otherWearable.WearableComponent.Item.HasTag(tag)) + { + hiddenByOtherItem = true; + break; + } + } + } + if (hiddenByOtherItem) { continue; } + } DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; @@ -1260,12 +1293,11 @@ namespace Barotrauma private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) { - var (finalColor, origin, rotation, scale, depth) - = CalculateDrawParameters(wearable, depthStep, color, alpha); - + if (wearable.Sprite?.Texture == null) { return; } + var (finalColor, origin, rotation, scale, depth) = CalculateDrawParameters(wearable, depthStep, color, alpha); var prevEffect = spriteBatch.GetCurrentEffect(); var alphaClipper = WearingItems.Find(w => w.AlphaClipOtherWearables); - bool shouldApplyAlphaClip = alphaClipper != null && wearable != alphaClipper; + bool shouldApplyAlphaClip = alphaClipper?.Sprite?.Texture != null && wearable != alphaClipper; if (shouldApplyAlphaClip) { ApplyAlphaClip(spriteBatch, wearable, alphaClipper, spriteEffect); @@ -1308,13 +1340,13 @@ namespace Barotrauma OtherWearables.ForEach(w => w.Sprite.Remove()); OtherWearables.Clear(); - HuskSprite?.Sprite.Remove(); + HuskSprite?.Sprite?.Remove(); HuskSprite = null; - HairWithHatSprite?.Sprite.Remove(); + HairWithHatSprite?.Sprite?.Remove(); HairWithHatSprite = null; - HerpesSprite?.Sprite.Remove(); + HerpesSprite?.Sprite?.Remove(); HerpesSprite = null; TintMask?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs new file mode 100644 index 000000000..014ccad7b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs @@ -0,0 +1,114 @@ +#nullable enable + +using System; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class CircuitBoxComponent + { + public static Option EditingHUD = Option.None; + + private Sprite Sprite => Item.Prefab.InventoryIcon ?? Item.Prefab.Sprite; + + private CircuitBoxLabel? label; + private CircuitBoxLabel Label + { + get + { + if (label is { } l) + { + return l; + } + + var name = TextManager.Get($"circuitboxnode.{Item.Prefab.Identifier}").Fallback($"[FALLBACK] {Item.Name}"); + label = new CircuitBoxLabel(name, GUIStyle.LargeFont); + return label.Value; + } + } + + public void UpdateEditing(RectTransform parent) + { + if (EditingHUD.TryUnwrap(out var editor)) + { + if (editor.UserData == this) { return; } + RemoveEditingHUD(); + } + EditingHUD = Option.Some(CreateEditingHUD(parent)); + } + + public static void RemoveEditingHUD() + { + if (!EditingHUD.TryUnwrap(out var editor)) { return; } + + editor.RectTransform.Parent = null; + EditingHUD = Option.None; + } + + public GUIComponent CreateEditingHUD(RectTransform parent) + { + GUIFrame frame = new(new RectTransform(new Vector2(0.4f, 0.3f), parent, Anchor.TopRight)) + { + UserData = this + }; + + GUIListBox listBox = new(new RectTransform(ToolBox.PaddingSizeParentRelative(frame.RectTransform, 0.8f), frame.RectTransform, Anchor.Center)) + { + KeepSpaceForScrollBar = true, + AutoHideScrollBar = false, + CanTakeKeyBoardFocus = false + }; + + bool isEditor = Screen.Selected is { IsEditor: true }; + + GUILayoutGroup titleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), listBox.Content.RectTransform)); + new GUITextBlock(new RectTransform(Vector2.One, titleHolder.RectTransform), Item.Name, font: GUIStyle.LargeFont) + { + TextColor = Color.White, + Color = Color.Black + }; + int fieldCount = 0; + + foreach (ItemComponent ic in Item.Components) + { + if (ic is Holdable) { continue; } + if (!ic.AllowInGameEditing) { continue; } + if (SerializableProperty.GetProperties(ic).Count == 0 && + !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) + { + continue; + } + + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); + + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont); + fieldCount += componentEditor.Fields.Count; + + ic.CreateEditingHUD(componentEditor); + componentEditor.Recalculate(); + } + + if (fieldCount == 0) + { + frame.Visible = false; + } + + return frame; + } + + public override void DrawHeader(SpriteBatch spriteBatch, RectangleF drawRect, Color color) + { + // scale to topRect height + Vector2 scale = new(drawRect.Height / MathF.Min(Sprite.size.X, Sprite.size.Y)), + spritePosition = new(drawRect.Left, drawRect.Top); + + float spriteWidth = Sprite.size.X * scale.X; + + Sprite.Draw(spriteBatch, spritePosition, Color.White, Vector2.Zero, 0f, scale); + GUI.DrawString(spriteBatch, new Vector2(spritePosition.X + spriteWidth + CircuitBoxSizes.NodeHeaderTextPadding, drawRect.Center.Y - Label.Size.Y / 2f), Label.Value, GUIStyle.TextColorNormal, font: GUIStyle.LargeFont); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs new file mode 100644 index 000000000..7fea1d1f1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs @@ -0,0 +1,127 @@ +#nullable enable + +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal abstract partial class CircuitBoxConnection + { + public string Name => Label.Value.Value; + public CircuitBoxLabel Label { get; private set; } + + private Sprite? knobSprite, + screwSprite, + connectorSprite; + + private static int padding => GUI.IntScale(8); + + private Option tooltip = Option.None; + + private partial void InitProjSpecific(CircuitBox circuitBox) + { + Label = new CircuitBoxLabel(Connection.Name, GUIStyle.SubHeadingFont); + knobSprite = circuitBox.ConnectionSprite; + screwSprite = circuitBox.ConnectionScrewSprite; + connectorSprite = circuitBox.WireConnectorSprite; + Length = Rect.Width + padding + Label.Size.X; + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color) + { + if (CircuitBox.UI is not { } circuitBoxUi) { return; } + var drawRect = CircuitBoxNode.OverrideRectLocation(Rect, drawPos, parentPos); + + Vector2 cursorPos = circuitBoxUi.GetCursorPosition(); + cursorPos.Y = -cursorPos.Y; + + bool isMouseOver = drawRect.Contains(cursorPos); + + float xPos; + if (IsOutput) + { + xPos = drawRect.Left - padding - Label.Size.X; + } + else + { + xPos = drawRect.Right + padding; + } + + Vector2 stringPos = new Vector2(xPos, drawRect.Center.Y - Label.Size.Y / 2f); + GUI.DrawString(spriteBatch, stringPos, Label.Value, GUIStyle.TextColorNormal, font: Label.Font); + + if (knobSprite is null) + { + CircuitBoxUI.DrawRectangleWithBorder(spriteBatch, drawRect, GUIStyle.Blue * 0.3f, GUIStyle.Blue); + } + else + { + float scale = drawRect.Height / knobSprite.size.Y; + knobSprite?.Draw(spriteBatch, drawRect.Center, color, 0f, scale); + } + + bool isScrewed = this switch + { + CircuitBoxOutputConnection output => output.ExternallyConnectedFrom.Count > 0, + CircuitBoxInputConnection input => input.ExternallyConnectedTo.Count > 0, + _ => Connection.Wires.Count > 0 || + Connection.CircuitBoxConnections.Count > 0 || + ExternallyConnectedFrom.Count > 0 + }; + + if (isMouseOver) + { + var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; + float glowScale = 40f / glowSprite.size.X; + if (isScrewed) + { + glowScale *= 1.2f; + } + glowSprite.Draw(spriteBatch, position, GUIStyle.Yellow, glowSprite.size / 2, scale: glowScale); + } + + tooltip = Option.None; + if (ConnectionPanel.ShouldDebugDrawWiring) + { + Connection.DrawConnectionDebugInfo(spriteBatch, Connection, drawRect.Center, isScrewed ? 1.1f : 0.9f, out var tooltipText); + + if (isMouseOver && !tooltipText.IsNullOrEmpty()) + { + tooltip = Option.Some(tooltipText); + } + } + + if (!isScrewed) { return; } + + if (screwSprite is not null) + { + float screwScale = drawRect.Height / screwSprite.size.Y; + screwSprite.Draw(spriteBatch, drawRect.Center, color, 0f, screwScale); + } + + if (connectorSprite is not null) + { + float screwScale = drawRect.Height / connectorSprite.size.Y * 2f; + Vector2 pos = drawRect.Center; + + connectorSprite.Draw(spriteBatch, + pos: pos, + color: Color.White, + origin: connectorSprite.Origin, + rotate: MathHelper.Pi / (IsOutput ? -2f : 2f), + scale: screwScale, + spriteEffect: SpriteEffects.None); + } + } + + public void DrawHUD(SpriteBatch spriteBatch, Camera camera) + { + if (!tooltip.TryUnwrap(out var text)) { return; } + + var drawPos = camera.WorldToScreen(new Vector2(Rect.Right, -Rect.Bottom)); + + GUIComponent.DrawToolTip(spriteBatch, text, drawPos); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs new file mode 100644 index 000000000..cd9013e64 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal readonly struct CircuitBoxLabel + { + public LocalizedString Value { get; } + + public Vector2 Size { get; } + + public GUIFont Font { get; } + + public CircuitBoxLabel(LocalizedString value, GUIFont font) + { + Value = value; + Font = font; + Size = font.MeasureString(font.ForceUpperCase ? value.Value.ToUpperInvariant() : value.Value); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs new file mode 100644 index 000000000..71d74c142 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs @@ -0,0 +1,233 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + /// + /// This class handles a couple things: + /// - Figuring out which components should be moved when dragging a certain part of the UI. + /// - Finding components, connectors and wires under cursor. + /// - Determines whether the user is dragging something. + /// + internal sealed class CircuitBoxMouseDragSnapshotHandler + { + public IEnumerable Nodes => circuitBoxUi.CircuitBox.Components.Union(circuitBoxUi.CircuitBox.InputOutputNodes); + + private IReadOnlyList Wires => circuitBoxUi.CircuitBox.Wires; + + // List of all connections in the circuit box + private ImmutableArray connections = ImmutableArray.Empty; + + // Nodes that were under cursor when dragging started + private ImmutableHashSet lastNodesUnderCursor = ImmutableHashSet.Empty, + // Nodes that were selected when dragging started + lastSelectedComponents = ImmutableHashSet.Empty, + // Nodes that should be moved when dragging + moveAffectedComponents = ImmutableHashSet.Empty; + + public ImmutableHashSet GetLastComponentsUnderCursor() => lastNodesUnderCursor; + public ImmutableHashSet GetMoveAffectedComponents() => moveAffectedComponents; + + public Option LastConnectorUnderCursor = Option.None; + public Option LastWireUnderCursor = Option.None; + + /// + /// If the user is currently dragging a node + /// + public bool IsDragging { get; private set; } + + /// + /// If the user is currently dragging a wire + /// + public bool IsWiring { get; private set; } + + private Vector2 startClick = Vector2.Zero; + private readonly CircuitBoxUI circuitBoxUi; + + /// + /// How far the user has to drag the mouse while holding down the button before dragging starts + /// + private const float dragTreshold = 16f; + + public CircuitBoxMouseDragSnapshotHandler(CircuitBoxUI ui) + { + circuitBoxUi = ui; + } + + /// + /// Called when the user holds down the mouse button + /// + public void StartDragging() + { + Vector2 cursorPos = circuitBoxUi.GetCursorPosition(); + SnapshotNodesUnderCursor(cursorPos); + SnapshotSelectedNodes(); + SnapshotMoveAffectedNodes(); + startClick = cursorPos; + } + + /// + /// Finds all connections and gathers them into a single list for easier iteration. + /// + public void UpdateConnections() + { + var builder = ImmutableArray.CreateBuilder(); + + builder.AddRange(circuitBoxUi.CircuitBox.Inputs); + builder.AddRange(circuitBoxUi.CircuitBox.Outputs); + + foreach (var node in Nodes) + { + builder.AddRange(node.Connectors); + } + + connections = builder.ToImmutable(); + } + + /// + /// Finds a possible connector under the cursor. + /// + public Option FindConnectorUnderCursor(Vector2 cursorPos) + { + foreach (var connection in connections) + { + if (connection.Contains(cursorPos)) + { + return Option.Some(connection); + } + } + + return Option.None; + } + + /// + /// Finds a possible wire under the cursor. + /// + public Option FindWireUnderCursor(Vector2 cursorPos) + { + foreach (CircuitBoxWire wire in Wires) + { + if (wire is { IsSelected: true, IsSelectedByMe: false }) { continue; } + if (wire.Renderer.Contains(cursorPos)) + { + return Option.Some(wire); + } + } + + return Option.None; + } + + /// + /// Find all nodes that are currently under the cursor that are not selected by someone else. + /// + public ImmutableHashSet FindNodesUnderCursor(Vector2 cursorPos) + { + var builder = ImmutableHashSet.CreateBuilder(); + foreach (var node in Nodes) + { + if (node is { IsSelected: true, IsSelectedByMe: false }) { continue; } + if (node.Rect.Contains(cursorPos)) + { + builder.Add(node); + } + } + + return builder.ToImmutable(); + } + + /// + /// Finds and stores all nodes, connectors and wires that are under the cursor when dragging starts. + /// + private void SnapshotNodesUnderCursor(Vector2 cursorPos) + { + lastNodesUnderCursor = FindNodesUnderCursor(cursorPos); + LastConnectorUnderCursor = FindConnectorUnderCursor(cursorPos); + LastWireUnderCursor = FindWireUnderCursor(cursorPos); + } + + /// + /// Stores all nodes that are currently selected when dragging starts. + /// There's no real way to change your selection while dragging so this is kinda pointless + /// but we snapshot it anyway just in case. + /// + private void SnapshotSelectedNodes() + { + lastSelectedComponents = Nodes.Where(static n => n is { IsSelected: true, IsSelectedByMe: true }).ToImmutableHashSet(); + } + + /// + /// Stores all nodes that should be moved when dragging starts. + /// + private void SnapshotMoveAffectedNodes() + { + bool moveSelection = lastNodesUnderCursor.Any(node => lastSelectedComponents.Contains(node)); + + /* + * If the user is dragging a selection, we should move all selected nodes (true). + * + * But for convenience, if the user is dragging a single node that is not part of the selection, + * we should move that node only instead and leave the selection alone. (false) + */ + moveAffectedComponents = moveSelection switch + { + true => lastSelectedComponents, + false => circuitBoxUi.GetTopmostNode(lastNodesUnderCursor) switch + { + null => ImmutableHashSet.Empty, + var node => ImmutableHashSet.Create(node) + } + }; + } + + public Vector2 GetDragAmount(Vector2 mousePos) => mousePos - startClick; + + /// + /// Called when the user releases the mouse button + /// + public void EndDragging() + { + startClick = Vector2.Zero; + IsDragging = false; + IsWiring = false; + lastNodesUnderCursor = ImmutableHashSet.Empty; + } + + public void UpdateDrag(Vector2 cursorPos) + { + // if there are no connectors under cursor, we can't be wiring anything + if (LastConnectorUnderCursor.IsNone()) + { + IsWiring = false; + } + + // if there are no nodes under cursor, we can't be dragging anything + if (lastNodesUnderCursor.IsEmpty) + { + IsDragging = false; + } + + // startClick is set to zero when the user releases the mouse button, so we should be neither dragging nor wiring in this state + if (startClick == Vector2.Zero) + { + IsDragging = false; + IsWiring = false; + return; + } + + bool isDragTresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; + + if (LastConnectorUnderCursor.IsNone()) + { + IsDragging |= isDragTresholdExceeded; + } + else + { + IsWiring |= isDragTresholdExceeded; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs new file mode 100644 index 000000000..b470f97f9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs @@ -0,0 +1,88 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class CircuitBoxNode + { + private RectangleF DrawRect, + TopDrawRect; + + private void UpdateDrawRects() + { + var drawRect = new RectangleF(Position - Size / 2f, Size); + drawRect.Y = -drawRect.Y; + drawRect.Y -= drawRect.Height; + DrawRect = drawRect; + + TopDrawRect = new RectangleF(drawRect.X, drawRect.Y - (CircuitBoxSizes.NodeHeaderHeight - 1), drawRect.Width, CircuitBoxSizes.NodeHeaderHeight); + } + + public void OnUICreated() + { + Size = CalculateSize(Connectors); + UpdatePositions(); + } + + public void DrawBackground(SpriteBatch spriteBatch, RectangleF drawRect, RectangleF topDrawRect, Color color) + { + CircuitBox.NodeFrameSprite?.Draw(spriteBatch, drawRect, color); + CircuitBox.NodeTopSprite?.Draw(spriteBatch, topDrawRect, color); + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Color color) + { + RectangleF drawRect = OverrideRectLocation(DrawRect, drawPos, Position), + topDrawRect = OverrideRectLocation(TopDrawRect, drawPos, Position); + + DrawBackground(spriteBatch, drawRect, topDrawRect, color); + DrawHeader(spriteBatch, topDrawRect, color); + + DrawConnectors(spriteBatch, drawPos); + } + + public void DrawHUD(SpriteBatch spriteBatch, Camera camera) + { + foreach (var c in Connectors) + { + c.DrawHUD(spriteBatch, camera); + } + } + + public virtual void DrawHeader(SpriteBatch spriteBatch, RectangleF rect, Color color) { } + + public void DrawConnectors(SpriteBatch spriteBatch, Vector2 drawPos) + { + var color = Color.White * Opacity; + foreach (var c in Connectors) + { + c.Draw(spriteBatch, drawPos, Position, color); + } + } + + public void DrawSelection(SpriteBatch spriteBatch, Color color) + { + int pad = GUI.IntScale(8); + + var rect = Rect; + rect.Y = -rect.Y; + rect.Y -= rect.Height; + + rect.Inflate(pad, pad); + + GUI.DrawFilledRectangle(spriteBatch, rect, color * Opacity); + } + + /// + /// Sets the location of the rectangle to a specific position, keeping origin intact. + /// + public static RectangleF OverrideRectLocation(RectangleF rect, Vector2 overridePos, Vector2 originalPos) + { + rect.Location -= new Vector2(originalPos.X, -originalPos.Y); + rect.Location += new Vector2(overridePos.X, -overridePos.Y); + return rect; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs new file mode 100644 index 000000000..a195b3276 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -0,0 +1,722 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Barotrauma +{ + internal sealed class CircuitBoxUI + { + private readonly Camera camera; + private static readonly Vector2 gridSize = new Vector2(128f); + public readonly CircuitBox CircuitBox; + private bool componentMenuOpen; + private float componentMenuOpenState; + + private GUICustomComponent? circuitComponent; + private GUIFrame? componentMenu; + private GUIButton? toggleMenuButton; + private GUIFrame? selectedWireFrame; + private GUIListBox? componentList; + private GUITextBlock? inventoryIndicatorText; + private readonly Sprite cursorSprite = GUIStyle.CursorSprite[CursorState.Default]; + + private Option selection = Option.None; + private string searchTerm = string.Empty; + + public static Option DraggedWire = Option.None; + + public readonly CircuitBoxMouseDragSnapshotHandler MouseSnapshotHandler; + + public List VirtualWires = new(); + + public CircuitBoxUI(CircuitBox box) + { + camera = new Camera + { + MinZoom = 0.25f, + MaxZoom = 2f + }; + + CircuitBox = box; + MouseSnapshotHandler = new CircuitBoxMouseDragSnapshotHandler(this); + } + +#region UI + + public void CreateGUI(GUIFrame parent) + { + GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.97f, 0.95f), parent.RectTransform, Anchor.Center), style: null); + circuitComponent = new GUICustomComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform), onDraw: (spriteBatch, component) => + { + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = component.Rect; + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); + DrawCircuits(spriteBatch); + spriteBatch.End(); + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + DrawHUD(spriteBatch); + spriteBatch.End(); + + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + }); + + GUIScissorComponent menuContainer = new GUIScissorComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform, anchor: Anchor.Center)) + { + CanBeFocused = false + }; + + componentMenuOpen = true; + componentMenu = new GUIFrame(new RectTransform(new Vector2(1f, 0.4f), menuContainer.Content.RectTransform, Anchor.BottomRight)); + toggleMenuButton = new GUIButton(new RectTransform(new Point(300, 30), GUI.Canvas) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") + { + OnClicked = (btn, userdata) => + { + componentMenuOpen = !componentMenuOpen; + foreach (GUIComponent child in btn.Children) + { + child.SpriteEffects = componentMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; + } + + return true; + } + }; + + GUILayoutGroup menuLayout = new GUILayoutGroup(new RectTransform(Vector2.One, componentMenu.RectTransform), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f }; + GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), menuLayout.RectTransform), isHorizontal: true); + + GUILayoutGroup labelLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true); + + GUILayoutGroup searchBarLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true); + GUITextBlock searchBarLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), searchBarLayout.RectTransform), "Filter"); + GUITextBox searchbar = new GUITextBox(new RectTransform(new Vector2(0.85f, 1f), searchBarLayout.RectTransform), string.Empty, createClearButton: true); + + new GUIFrame(new RectTransform(new Vector2(0.5f, 0.01f), menuLayout.RectTransform), style: "HorizontalLine"); + + componentList = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.65f), menuLayout.RectTransform)) + { + PlaySoundOnSelect = true, + UseGridLayout = true, + OnSelected = (_, o) => + { + if (o is not ItemPrefab prefab) { return false; } + + CircuitBox.HeldComponent = Option.Some(prefab); + return true; + } + }; + + GUILayoutGroup inventoryLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); + GUILayoutGroup indicatorLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1f), inventoryLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUIImage indicatorIcon = new GUIImage(new RectTransform(new Vector2(0.5f, 0.8f), indicatorLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CircuitIndicatorIcon"); + inventoryIndicatorText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), indicatorLayout.RectTransform), GetInventoryText(), font: GUIStyle.SubHeadingFont); + + int gapSize = GUI.IntScale(8); + selectedWireFrame = SubEditorScreen.CreateWiringPanel(Point.Zero, SelectWire); + selectedWireFrame.RectTransform.AbsoluteOffset = new Point(parent.Rect.X - (selectedWireFrame.Rect.Width + gapSize), parent.Rect.Y); + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(static p => p.Name)) + { + if (!prefab.Tags.Contains("circuitboxcomponent")) { continue; } + + CreateComponentElement(prefab, componentList.Content.RectTransform); + } + + searchbar.OnTextChanged += (tb, s) => + { + searchTerm = s; + UpdateComponentList(); + return true; + }; + int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), parent.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + style: "GUIButtonSettings") + { + OnClicked = (btn, userdata) => + { + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") + }, + new ContextMenuOption("circuitboxsetting.find", isEnabled: true, + new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Input)) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") + }, + new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Output)) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") + }, + new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") + })); + + + void ResetCamera() + { + // Vector2.One because Vector2.Zero means no value + camera.TargetPos = Vector2.One; + } + + void FindInputOuput(CircuitBoxInputOutputNode.Type type) + { + var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); + if (input is null) { return; } + + camera.TargetPos = input.Position; + } + + void FindCircuit() + { + var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); + if (closestComponent is null) { return; } + + camera.TargetPos = closestComponent.Position; + } + return true; + } + }; + + MouseSnapshotHandler.UpdateConnections(); + + // update scales of everything + foreach (var node in CircuitBox.Components) { node.OnUICreated(); } + foreach (var node in CircuitBox.InputOutputNodes) { node.OnUICreated(); } + foreach (var wire in CircuitBox.Wires) { wire.Update(); } + } + + private string GetInventoryText() + => CircuitBox.ComponentContainer is { } container + ? $"{container.Inventory.AllItems.Count()}/{container.Capacity}" + : "0/0"; + + public void UpdateComponentList() + { + if (inventoryIndicatorText is { } text) + { + text.Text = GetInventoryText(); + } + + if (componentList is null) { return; } + var playerInventory = CircuitBox.GetSortedCircuitBoxSortedItemsFromPlayer(Character.Controlled); + + foreach (GUIComponent child in componentList.Content.Children) + { + if (child.UserData is not ItemPrefab prefab) { continue; } + + child.Enabled = !CircuitBox.IsFull && (!CircuitBox.IsInGame() || CircuitBox.GetApplicableResourcePlayerHas(prefab, playerInventory).IsSome()); + + if (child.GetChild()?.GetChild() is { } image) + { + image.Enabled = child.Enabled; + } + + child.ToolTip = child.Enabled + ? prefab.Description + : RichString.Rich(TextManager.GetWithVariable(new Identifier("CircuitBoxUIComponentNotAvailable"), new Identifier("[item]"), prefab.Name)); + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + child.Visible = true; + continue; + } + + child.Visible = prefab.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); + } + } + + private static bool SelectWire(GUIComponent component, object obj) + { + if (obj is not ItemPrefab prefab) { return false; } + + CircuitBoxWire.SelectedWirePrefab = prefab; + return true; + } + + private static void CreateComponentElement(ItemPrefab prefab, RectTransform parent) + { + GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), parent) { MinSize = new Point(0, 50) }, style: "GUITextBox") + { + UserData = prefab + }; + + itemFrame.RectTransform.MinSize = new Point(0, itemFrame.Rect.Width); + itemFrame.RectTransform.MaxSize = new Point(int.MaxValue, itemFrame.Rect.Width); + itemFrame.ToolTip = prefab.Name; + + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), itemFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.03f, + CanBeFocused = false + }; + + Sprite icon; + Color iconColor; + + if (prefab.InventoryIcon != null) + { + icon = prefab.InventoryIcon; + iconColor = prefab.InventoryIconColor; + } + else + { + icon = prefab.Sprite; + iconColor = prefab.SpriteColor; + } + + GUIImage? img = null; + if (icon != null) + { + img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) + { + CanBeFocused = false, + LoadAsynchronously = true, + DisabledColor = Color.DarkGray * 0.8f, + Color = iconColor + }; + } + + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), + text: prefab.Name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont) + { + CanBeFocused = false + }; + + textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + paddedFrame.Recalculate(); + + if (img != null) + { + img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); + img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); + } + } + +#endregion + + private void DrawHUD(SpriteBatch spriteBatch) + { + float scale = GUI.Scale / 1.5f; + Vector2 offset = new Vector2(20, 40) * scale; + + foreach (var (character, cursor) in CircuitBox.ActiveCursors) + { + if (!cursor.IsActive) { continue; } + + Vector2 cursorWorldPos = camera.WorldToScreen(cursor.DrawPosition); + + if (cursor.Info.DragStart.TryUnwrap(out Vector2 dragStart)) + { + DrawSelection(spriteBatch, dragStart, cursor.DrawPosition, cursor.Color); + } + + if (cursor.HeldPrefab.TryUnwrap(out ItemPrefab? otherHeldPrefab)) + { + otherHeldPrefab.Sprite.Draw(spriteBatch, cursorWorldPos); + } + + cursorSprite?.Draw(spriteBatch, cursorWorldPos, cursor.Color, 0f, scale); + GUI.DrawString(spriteBatch, cursorWorldPos + offset, character.Name, cursor.Color, Color.Black, GUI.IntScale(4), GUIStyle.SmallFont); + } + + if (selection.TryUnwrap(out RectangleF rect)) + { + Vector2 pos1 = rect.Location; + Vector2 pos2 = new Vector2(rect.Location.X + rect.Size.X, rect.Location.Y + rect.Size.Y); + DrawSelection(spriteBatch, pos1, pos2, GUIStyle.Blue); + } + + if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? component)) + { + component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition); + } + + foreach (var c in CircuitBox.Components) + { + c.DrawHUD(spriteBatch, camera); + } + + foreach (var n in CircuitBox.InputOutputNodes) + { + n.DrawHUD(spriteBatch, camera); + } + } + + private void DrawSelection(SpriteBatch spriteBatch, Vector2 pos1, Vector2 pos2, Color color) + { + Vector2 location = camera.WorldToScreen(pos1); + location.Y = -location.Y; + Vector2 location2 = camera.WorldToScreen(pos2); + location2.Y = -location2.Y; + MapEntity.DrawSelectionRect(spriteBatch, location, new Vector2(-(location.X - location2.X), location.Y - location2.Y), color); + } + + private const float lineBaseWidth = 1f; + private static float lineWidth; + + public static void DrawRectangleWithBorder(SpriteBatch spriteBatch, RectangleF rect, Color fillColor, Color borderColor) + { + Vector2 topRight = new Vector2(rect.Right, rect.Top), + topLeft = new Vector2(rect.Left, rect.Top), + bottomRight = new Vector2(rect.Right, rect.Bottom), + bottomLeft = new Vector2(rect.Left, rect.Bottom); + + Vector2 offset = new Vector2(0f, lineWidth / 2f); + + GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); + + spriteBatch.DrawLine(topRight, topLeft, borderColor, thickness: lineWidth); + spriteBatch.DrawLine(topLeft - offset, bottomLeft + offset, borderColor, thickness: lineWidth); + spriteBatch.DrawLine(bottomLeft, bottomRight, borderColor, thickness: lineWidth); + spriteBatch.DrawLine(bottomRight + offset, topRight - offset, borderColor, thickness: lineWidth); + } + + private void DrawCircuits(SpriteBatch spriteBatch) + { + camera.UpdateTransform(interpolate: true, updateListener: false); + SubEditorScreen.DrawOutOfBoundsArea(spriteBatch, camera, CircuitBoxSizes.PlayableAreaSize, GUIStyle.Red * 0.33f); + SubEditorScreen.DrawGrid(spriteBatch, camera, gridSize.X, gridSize.Y, zoomTreshold: false); + lineWidth = lineBaseWidth / camera.Zoom; + + Vector2 mousePos = GetCursorPosition(); + mousePos.Y = -mousePos.Y; + + foreach (CircuitBoxWire wire in CircuitBox.Wires) + { + wire.Renderer.Draw(spriteBatch, GetSelectionColor(wire)); + } + + foreach (var node in CircuitBox.Components) + { + if (node.IsSelected) + { + node.DrawSelection(spriteBatch, GetSelectionColor(node)); + } + + node.Draw(spriteBatch, node.Position, node.Item.Prefab.SignalComponentColor * CircuitBoxNode.Opacity); + } + + foreach (var ioNode in CircuitBox.InputOutputNodes) + { + if (ioNode.IsSelected) + { + ioNode.DrawSelection(spriteBatch, GetSelectionColor(ioNode)); + } + + Color color = ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red; + ioNode.Draw(spriteBatch, ioNode.Position, color * CircuitBoxNode.Opacity); + } + + if (MouseSnapshotHandler.IsDragging) + { + var draggedNodes = MouseSnapshotHandler.GetMoveAffectedComponents(); + Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition()); + foreach (CircuitBoxNode moveable in draggedNodes) + { + Color color = moveable switch + { + CircuitBoxComponent node => node.Item.Prefab.SignalComponentColor, + CircuitBoxInputOutputNode ioNode => ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red, + _ => Color.White + }; + moveable.Draw(spriteBatch, moveable.Position + dragOffset, color * 0.5f); + } + } + + if (DraggedWire.TryUnwrap(out CircuitBoxWireRenderer? draggedWire)) + { + draggedWire.Draw(spriteBatch, GUIStyle.Yellow); + } + } + + private Color GetSelectionColor(CircuitBoxNode node) + => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); + + private Color GetSelectionColor(CircuitBoxWire wire) + => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); + + private Color GetSelectionColor(ushort selectedBy, bool isSelectedByMe) + { +#if !DEBUG + if (isSelectedByMe) + { + return GUIStyle.Yellow; + } +#endif + + foreach (var (_, cursor) in CircuitBox.ActiveCursors) + { + if (cursor.Info.CharacterID == selectedBy) + { + return cursor.Color; + } + } + + return GUIStyle.Yellow; + } + + private Vector2 cursorPos; + public Vector2 GetCursorPosition() => cursorPos; + public Option GetDragStart() => selection.Select(static f => f.Location); + + public void Update(float deltaTime) + { + cursorPos = camera.ScreenToWorld(PlayerInput.MousePosition); + foreach (CircuitBoxWire wire in CircuitBox.Wires) + { + wire.Update(); + } + + bool foundSelected = false; + foreach (var node in CircuitBox.Components) + { + if (!node.IsSelectedByMe) { continue; } + + foundSelected = true; + if (circuitComponent is not null) + { + node.UpdateEditing(circuitComponent.RectTransform); + } + break; + } + + if (!foundSelected) + { + CircuitBoxComponent.RemoveEditingHUD(); + } + + bool isMouseOn = GUI.MouseOn == circuitComponent; + + if (isMouseOn) + { + Character.DisableControls = true; + } + camera.MoveCamera(deltaTime, allowMove: true, allowZoom: isMouseOn, allowInput: isMouseOn, followSub: false); + + if (camera.TargetPos != Vector2.Zero && MathUtils.NearlyEqual(camera.Position, camera.TargetPos, 0.01f)) + { + camera.TargetPos = Vector2.Zero; + } + + if (isMouseOn) + { + if (CircuitBox.HeldComponent.IsNone() && PlayerInput.PrimaryMouseButtonDown()) + { + MouseSnapshotHandler.StartDragging(); + } + + if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld())) + { + Vector2 moveSpeed = PlayerInput.MouseSpeed / camera.Zoom; + moveSpeed.X = -moveSpeed.X; + camera.Position += moveSpeed; + } + + if (PlayerInput.PrimaryMouseButtonHeld()) + { + MouseSnapshotHandler.UpdateDrag(GetCursorPosition()); + } + + if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var c)) + { + Vector2 start = c.Rect.Center, + end = GetCursorPosition(); + + end.Y = -end.Y; + + if (!c.IsOutput) + { + (start, end) = (end, start); + } + + if (DraggedWire.TryUnwrap(out var wire)) + { + wire.Recompute(start, end, CircuitBoxWire.SelectedWirePrefab.SpriteColor); + } + else + { + DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None,start, end, GUIStyle.Red, CircuitBox.WireSprite)); + } + } + else + { + DraggedWire = Option.None; + } + + if (PlayerInput.SecondaryMouseButtonClicked()) + { + OpenContextMenu(); + } + + if (PlayerInput.PrimaryMouseButtonClicked()) + { + if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? prefab)) + { + CircuitBox.AddComponent(prefab, cursorPos); + } + else + { + if (MouseSnapshotHandler.IsDragging && PlayerInput.PrimaryMouseButtonReleased()) + { + CircuitBox.MoveComponent(MouseSnapshotHandler.GetDragAmount(cursorPos), MouseSnapshotHandler.GetMoveAffectedComponents()); + } + else if (!MouseSnapshotHandler.IsWiring) + { + TrySelectComponentsUnderCursor(); + } + } + + if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var one)) + { + if (MouseSnapshotHandler.FindConnectorUnderCursor(cursorPos).TryUnwrap(out var two)) + { + CircuitBox.AddWire(one, two); + } + } + + CircuitBox.SelectWires(MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) ? ImmutableArray.Create(wire) : ImmutableArray.Empty, !PlayerInput.IsShiftDown()); + + CircuitBox.HeldComponent = Option.None; + MouseSnapshotHandler.EndDragging(); + } + + if (MouseSnapshotHandler.GetLastComponentsUnderCursor().IsEmpty && MouseSnapshotHandler.LastConnectorUnderCursor.IsNone()) + { + UpdateSelection(); + } + + // Allow using both Delete key and Ctrl+D for those who don't have a Delete key + bool hitDeleteCombo = PlayerInput.KeyHit(Keys.Delete) || (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.D)); + + if (GUI.KeyboardDispatcher.Subscriber is null && hitDeleteCombo) + { + CircuitBox.RemoveComponents(CircuitBox.Components.Where(static node => node.IsSelectedByMe).ToArray()); + CircuitBox.RemoveWires(CircuitBox.Wires.Where(static wire => wire.IsSelectedByMe).ToImmutableArray()); + } + } + + if (componentMenu is { } menu && toggleMenuButton is { } button) + { + componentMenuOpenState = componentMenuOpen ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); + + menu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, menu.Rect.Height - 10), Vector2.Zero, componentMenuOpenState).ToPoint(); + button.RectTransform.AbsoluteOffset = new Point(menu.Rect.X + ((menu.Rect.Width / 2) - (button.Rect.Width / 2)), menu.Rect.Y - button.Rect.Height); + } + + camera.Position = Vector2.Clamp(camera.Position, + new Vector2(-CircuitBoxSizes.PlayableAreaSize / 2f), + new Vector2(CircuitBoxSizes.PlayableAreaSize / 2f)); + } + + private void UpdateSelection() + { + if (!PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonDown()) + { + selection = Option.Some(new RectangleF(GetCursorPosition(), Vector2.Zero)); + } + + if (!selection.TryUnwrap(out RectangleF rect)) { return; } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + selection = Option.None; + RectangleF selectionRect = Submarine.AbsRectF(rect.Location, rect.Size); + + float treshold = 12f / camera.Zoom; + if (selectionRect.Size.X < treshold || selectionRect.Size.Y < treshold) { return; } + + CircuitBox.SelectComponents(MouseSnapshotHandler.Nodes.Where(n => selectionRect.Intersects(n.Rect)).ToImmutableHashSet(), !PlayerInput.IsShiftDown()); + } + else + { + RectangleF oldRect = rect; + rect.Size = camera.ScreenToWorld(PlayerInput.MousePosition) - rect.Location; + if (rect.Equals(oldRect)) { return; } + + selection = Option.Some(rect); + } + } + + private void TrySelectComponentsUnderCursor() + { + CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor()); + + CircuitBox.SelectComponents(foundNode is null ? ImmutableArray.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown()); + } + + private void OpenContextMenu() + { + var wireOption = MouseSnapshotHandler.FindWireUnderCursor(cursorPos); + var wireSelection = CircuitBox.Wires.Where(static w => w.IsSelectedByMe).ToImmutableArray(); + var nodeOption = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos)); + var nodeSelection = CircuitBox.Components.Where(static n => n.IsSelectedByMe).ToImmutableArray(); + + var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: wireOption.IsSome() || nodeOption is CircuitBoxComponent, () => + { + if (wireOption.TryUnwrap(out var wire)) + { + CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire)); + } + + if (nodeOption is CircuitBoxComponent node) + { + CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); + } + }); + + // show component name in the header to better indicate what is about to be deleted + if (nodeOption is CircuitBoxComponent comp) + { + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, option); + return; + } + + // also check if a wire is being deleted + if (wireOption.TryUnwrap(out var foundWire)) + { + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, option); + return; + } + + GUIContextMenu.CreateContextMenu(option); + } + + public CircuitBoxNode? GetTopmostNode(ImmutableHashSet nodes) + { + CircuitBoxNode? foundNode = null; + + var allNodes = MouseSnapshotHandler.Nodes.ToImmutableArray(); + + for (int i = allNodes.Length - 1; i >= 0; i--) + { + CircuitBoxNode node = allNodes[i]; + + if (nodes.Contains(node)) + { + foundNode = node; + break; + } + } + + return foundNode; + } + + public void AddToGUIUpdateList() + { + toggleMenuButton?.AddToGUIUpdateList(); + selectedWireFrame?.AddToGUIUpdateList(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs new file mode 100644 index 000000000..26cfbad79 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Barotrauma +{ + internal partial class CircuitBoxWire + { + public CircuitBoxWireRenderer Renderer; + + public void Update() => Renderer.Recompute(From.AnchorPoint, To.AnchorPoint, Color); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs new file mode 100644 index 000000000..7b02ec1e2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs @@ -0,0 +1,271 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal class CircuitBoxWireRenderer + { + private const int VertsPerQuad = 4, // how many points per quad + QuadsPerLine = 10, // how many quads per line + VertsPerLine = QuadsPerLine * VertsPerQuad, // how many points we need to draw all the quads for a single line + TotalVertsPerWire = VertsPerLine * 2; // we are drawing 2 lines + + private readonly Texture2D texture; + + private VertexPositionColorTexture[] verts = new VertexPositionColorTexture[TotalVertsPerWire]; + private readonly Vector2[][] colliders = new Vector2[2][]; + private SquareLine skeleton; + + private Vector2 lastStart, lastEnd; + private Color lastColor; + private readonly Option wire; + + public CircuitBoxWireRenderer(Option wire, Vector2 start, Vector2 end, Color color, Sprite? wireSprite) + { + this.wire = wire; + texture = wireSprite?.Texture ?? GUI.WhiteTexture; + Recompute(start, end, color); + } + + private void UpdateColor(Color color) + { + for (int i = 0; i < TotalVertsPerWire; i++) + { + verts[i].Color = color; + } + + lastColor = color; + } + + public void Recompute(Vector2 start, Vector2 end, Color color) + { + if (MathUtils.NearlyEqual(lastStart, start) && MathUtils.NearlyEqual(lastEnd, end)) + { + if (lastColor == color) { return; } + + UpdateColor(color); + return; + } + + lastStart = start; + lastEnd = end; + lastColor = color; + + skeleton = ToolBox.GetSquareLineBetweenPoints(start, end, CircuitBoxSizes.WireKnobLength); + var points = skeleton.Points; + + Vector2 centerOfLine = (points[2] + points[3]) / 2f; + + ImmutableArray points1 = GetLinePoints(points[1], points[2], centerOfLine), + points2 = GetLinePoints(centerOfLine, points[3], points[4]); + + colliders[0] = ConstructQuads(ref verts, 0, points1, color); + colliders[1] = ConstructQuads(ref verts, VertsPerLine, points2, color); + + static ImmutableArray GetLinePoints(Vector2 start, Vector2 control, Vector2 end) + { + var points = ImmutableArray.CreateBuilder(QuadsPerLine); + for (int i = 0; i < QuadsPerLine; i++) + { + float t = (float)i / (QuadsPerLine - 1); + Vector2 pos = MathUtils.Bezier(start, control, end, t); + points.Add(pos); + } + + return points.ToImmutable(); + } + + static Vector2[] ConstructQuads(ref VertexPositionColorTexture[] verts, int startOffset, IReadOnlyList points, Color color) + { + // ok I don't know why this needs to be one quad less, maybe we are drawing with only 9 quads lol + var collider = new Vector2[VertsPerLine - VertsPerQuad]; + + int leftIndex = collider.Length - 1, + rightIndex = 0; + + // we need to calculate half of the width since the way we expand the quads from origin, otherwise the line will be twice as wide + const float halfWidth = CircuitBoxSizes.WireWidth / 2f; + + // draw the line using quads + for (int i = 0; i < points.Count - 1; i++) + { + bool isFirst = i == 0 && startOffset == 0, + isLast = i == points.Count - 2 && startOffset > 0; + + Vector2 start = points[i], + end = points[i + 1]; + + Vector2 dir = Vector2.Normalize(end - start); + Vector2 length = new Vector2(dir.Y, -dir.X) * halfWidth; + + int vertIndex = startOffset + i * 4; + + Vector2 topRight = end + length; + Vector2 topLeft = end - length; + + Vector2 bottomRight; + Vector2 bottomLeft; + + // get previous points if any + int prevIndex = vertIndex - 4; + + if ((prevIndex - startOffset) >= 0) + { + // connect the previous "upper" corners into the current "lower" corners to stitch the line together + Vector3 prevTopRight = verts[TopRight(prevIndex)].Position, + prevTopLeft = verts[TopLeft(prevIndex)].Position; + + bottomRight = ToVector2(prevTopRight); + bottomLeft = ToVector2(prevTopLeft); + } + else + { + bottomRight = start + length; + bottomLeft = start - length; + } + + if (isFirst) + { + if (MathF.Abs(dir.Y) > MathF.Abs(dir.X)) + { + float offset = dir.Y < 0 ? halfWidth : -halfWidth; + // if the line is more vertical than horizontal, we want to move the bottom corners to the left + bottomRight.Y = start.Y - offset; + bottomLeft.Y = start.Y - offset; + } + else + { + // otherwise we want to move the bottom corners to the top + bottomRight.X = start.X; + bottomLeft.X = start.X; + } + } + else if (isLast) + { + if (MathF.Abs(dir.Y) > MathF.Abs(dir.X)) + { + float offset = dir.Y < 0 ? halfWidth : -halfWidth; + // if the line is more vertical than horizontal, we want to move the bottom corners to the left + topRight.Y = end.Y + offset; + topLeft.Y = end.Y + offset; + } + else + { + // otherwise we want to move the bottom corners to the top + topRight.X = end.X; + topLeft.X = end.X; + } + } + + collider[rightIndex++] = bottomLeft; + collider[rightIndex++] = topLeft; + + collider[leftIndex--] = bottomRight; + collider[leftIndex--] = topRight; + + // adjust this if we want sprites to support sourceRects + Vector2 uvTopRight = new Vector2(0, 1), + uvTopLeft = new Vector2(0, 0), + uvBottomRight = new Vector2(1, 1), + uvBottomLeft = new Vector2(1, 0); + + SetPos(ref verts, TopRight(vertIndex), topRight, color, uvTopRight); + SetPos(ref verts, TopLeft(vertIndex), topLeft, color, uvTopLeft); + SetPos(ref verts, BottomRight(vertIndex), bottomRight, color, uvBottomRight); + SetPos(ref verts, BottomLeft(vertIndex), bottomLeft, color, uvBottomLeft); + + static void SetPos(ref VertexPositionColorTexture[] verts, int index, Vector2 pos, Color color, Vector2 uv) + { + verts[index].Position = ToVector3(pos); + verts[index].Color = color; + verts[index].TextureCoordinate = uv; + static Vector3 ToVector3(Vector2 v) => new Vector3(v.X, v.Y, 0f); + } + + static int TopRight(int vertIndex) => vertIndex; + static int TopLeft(int vertIndex) => vertIndex + 1; + static int BottomRight(int vertIndex) => vertIndex + 2; + static int BottomLeft(int vertIndex) => vertIndex + 3; + + static Vector2 ToVector2(Vector3 v) => new Vector2(v.X, v.Y); + } + + return collider; + } + } + + public bool Contains(Vector2 pos) + { + pos.Y = -pos.Y; + foreach (Vector2[] collider in colliders) + { + if (ToolBox.PointIntersectsWithPolygon(pos, collider, checkBoundingBox: false)) { return true; } + } + + return false; + } + + public void Draw(SpriteBatch spriteBatch, Color selectionColor) + { + if (GameMain.DebugDraw) + { + for (int i = 0; i < skeleton.Points.Length; i++) + { + Vector2 point = skeleton.Points[i]; + spriteBatch.DrawPoint(point, Color.White, 25f); + GUI.DrawString(spriteBatch, point - new Vector2(5f, 17f), i.ToString(), Color.Black, font: GUIStyle.LargeFont); + } + + spriteBatch.DrawLine(skeleton.Points[0], skeleton.Points[1], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[1], skeleton.Points[2], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[2], skeleton.Points[3], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[3], skeleton.Points[4], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[4], skeleton.Points[5], GUIStyle.Green, thickness: 2f); + } + + bool isSelected = wire.TryUnwrap(out var w) && w.IsSelected; + + if (isSelected) + { + foreach (var colliderPolys in colliders) + { + spriteBatch.DrawPolygon(Vector2.Zero, colliderPolys, selectionColor, 5f); + } + } + + spriteBatch.Draw(texture, verts, 0f); + + if (skeleton.Type is SquareLine.LineType.SixPointBackwardsLine) + { + // we need to expand the start and end points to make the line look like it's connected to the "smooth" part of the line + Vector2 expandedEnd = skeleton.Points[1], + expandedStart = skeleton.Points[4]; + + expandedEnd.X += CircuitBoxSizes.WireWidth / 2f; + expandedStart.X -= CircuitBoxSizes.WireWidth / 2f; + + spriteBatch.DrawLineWithTexture(texture, skeleton.Points[0], expandedEnd, lastColor, thickness: CircuitBoxSizes.WireWidth); + spriteBatch.DrawLineWithTexture(texture, expandedStart, skeleton.Points[5], lastColor, thickness: CircuitBoxSizes.WireWidth); + + const float rectSize = CircuitBoxSizes.WireWidth * 1.5f; + RectangleF startKnob = new RectangleF(skeleton.Points[1] - new Vector2(rectSize / 2f), new Vector2(rectSize)), + endKnob = new RectangleF(skeleton.Points[4] - new Vector2(rectSize / 2f), new Vector2(rectSize)); + + GUI.DrawFilledRectangle(spriteBatch, startKnob, lastColor); + GUI.DrawFilledRectangle(spriteBatch, endKnob, lastColor); + } + + if (!GameMain.DebugDraw) { return; } + + foreach (var colliderPolys in colliders) + { + spriteBatch.DrawPolygonInner(Vector2.Zero, colliderPolys, Color.Lime, 1f); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 908ce4426..d355c7cb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -315,7 +315,7 @@ namespace Barotrauma else { var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - msg.Text, font: GUIStyle.SmallFont, wrap: true) + RichString.Rich(msg.Text), font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -403,7 +403,7 @@ namespace Barotrauma { if (Screen.Selected != GameMain.SubEditorScreen) return; - if (MapEntity.mapEntityList.Any(e => e is Hull || e is Gap)) + if (MapEntity.MapEntityList.Any(e => e is Hull || e is Gap)) { ShowQuestionPrompt("This submarine already has hulls and/or gaps. This command will delete them. Do you want to continue? Y/N", (option) => @@ -974,7 +974,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { bool entityFound = false; - foreach (MapEntity entity in MapEntity.mapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { if (entity is Item item) { @@ -1096,19 +1096,19 @@ namespace Barotrauma commands.Add(new Command("cleansub", "", (string[] args) => { - for (int i = MapEntity.mapEntityList.Count - 1; i >= 0; i--) + for (int i = MapEntity.MapEntityList.Count - 1; i >= 0; i--) { - MapEntity me = MapEntity.mapEntityList[i]; + MapEntity me = MapEntity.MapEntityList[i]; if (me.SimPosition.Length() > 2000.0f) { NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange); - MapEntity.mapEntityList.RemoveAt(i); + MapEntity.MapEntityList.RemoveAt(i); } else if (!me.ShouldBeSaved) { NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange); - MapEntity.mapEntityList.RemoveAt(i); + MapEntity.MapEntityList.RemoveAt(i); } else if (me is Item) { @@ -1477,14 +1477,19 @@ namespace Barotrauma string itemNameOrId = args[0].ToLowerInvariant(); ItemPrefab itemPrefab = - (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId.ToIdentifier(), showErrorMessages: false)) as ItemPrefab; + (MapEntityPrefab.FindByName(itemNameOrId) ?? + MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab; if (itemPrefab == null) { NewMessage("Item not found for analyzing."); return; } + if (itemPrefab.DefaultPrice == null) + { + NewMessage($"Item \"{itemPrefab.Name}\" is not sellable/purchaseable."); + return; + } NewMessage("Analyzing item " + itemPrefab.Name + " with base cost " + itemPrefab.DefaultPrice.Price); var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab); @@ -1847,6 +1852,12 @@ namespace Barotrauma } } + foreach (var eventPrefab in EventPrefab.Prefabs) + { + if (eventPrefab is not TraitorEventPrefab traitorEventPrefab) { continue; } + addIfMissing($"eventname.{traitorEventPrefab.Identifier}".ToIdentifier(), language); + } + foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) { checkSerializableEntityType(itemComponentType); @@ -2346,7 +2357,20 @@ namespace Barotrauma WaterRenderer.BlurAmount = blurAmount; })); - + commands.Add(new Command("generatelevels", "generatelevels [amount]: generate a bunch of levels with the currently selected parameters in the level editor.", (string[] args) => + { + if (GameMain.GameSession == null) + { + int amount = 1; + if (args.Length > 0) { int.TryParse(args[0], out amount); } + GameMain.LevelEditorScreen.TestLevelGenerationForErrors(amount); + } + else + { + NewMessage("Can't use the command while round is running."); + } + })); + commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) => { //TODO: maybe do this automatically during loading when possible? @@ -2528,8 +2552,11 @@ namespace Barotrauma HashSet docs = new HashSet(); HashSet textIds = new HashSet(); + Dictionary existingTexts = new Dictionary(); + foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs()) { + if (eventPrefab is not TraitorEventPrefab) { continue; } if (eventPrefab.Identifier.IsEmpty) { continue; @@ -2566,35 +2593,74 @@ namespace Barotrauma void getTextsFromElement(XElement element, List list, string parentName) { string text = element.GetAttributeString("text", null); - string textId = $"EventText.{parentName}"; - if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase)) - { - list.Add($"<{textId}>{text}"); - element.SetAttributeValue("text", textId); + string textAttribute = "text"; + XElement textElement = element; + if (text == null) + { + var subTextElement = element?.Element("Text"); + if (subTextElement != null) + { + textAttribute = "tag"; + text = subTextElement?.GetAttributeString(textAttribute, null); + textElement = subTextElement; + } + if (text == null) + { + AddWarning("Failed to find text from the element " + element.ToString()); + } } - int i = 1; + string textId = $"EventText.{parentName}"; + if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase)) + { + if (existingTexts.TryGetValue(text, out string existingTextId)) + { + textElement.SetAttributeValue(textAttribute, existingTextId); + } + else + { + textIds.Add(parentName); + list.Add($"<{textId}>{text}"); + existingTexts.Add(text, textId); + textElement.SetAttributeValue(textAttribute, textId); + } + } + + int conversationIndex = 1; + int objectiveIndex = 1; foreach (var subElement in element.Elements()) { + string elementName = parentName; + bool ignore = false; switch (subElement.Name.ToString().ToLowerInvariant()) { - case "conversationaction": - while (textIds.Contains(parentName+".c"+i)) + case "conversationaction": + while (textIds.Contains(elementName + ".c" + conversationIndex)) { - i++; + conversationIndex++; } - parentName += ".c" + i; + elementName += ".c" + conversationIndex; + break; + case "eventlogaction": + while (textIds.Contains(elementName + ".objective" + objectiveIndex)) + { + objectiveIndex++; + } + elementName += ".objective" + objectiveIndex; break; case "option": - while (textIds.Contains(parentName.Substring(0, parentName.Length - 3) + ".o" + i)) + while (textIds.Contains(elementName.Substring(0, elementName.Length - 3) + ".o" + conversationIndex)) { - i++; + conversationIndex++; } - parentName = parentName.Substring(0, parentName.Length - 3) + ".o" + i; + elementName = elementName.Substring(0, elementName.Length - 3) + ".o" + conversationIndex; + break; + case "text": + ignore = true; break; } - textIds.Add(parentName); - getTextsFromElement(subElement, list, parentName); + if (ignore) { continue; } + getTextsFromElement(subElement, list, elementName); } } })); @@ -2759,9 +2825,9 @@ namespace Barotrauma ep.DebugCreateInstance(); } - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + for (int i = 0; i < MapEntity.MapEntityList.Count; i++) { - var entity = MapEntity.mapEntityList[i] as ISerializableEntity; + var entity = MapEntity.MapEntityList[i] as ISerializableEntity; if (entity != null) { List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 0d7bb2e49..45893dc8b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -42,15 +42,15 @@ namespace Barotrauma partial void ShowDialog(Character speaker, Character targetCharacter) { - CreateDialog(Text, speaker, Options.Select(opt => opt.Text), GetEndingOptions(), actionInstance: this, spriteIdentifier: EventSprite, fadeToBlack: FadeToBlack, dialogType: DialogType, continueConversation: ContinueConversation); + CreateDialog(GetDisplayText(), speaker, Options.Select(opt => opt.Text), GetEndingOptions(), actionInstance: this, spriteIdentifier: EventSprite, fadeToBlack: FadeToBlack, dialogType: DialogType, continueConversation: ContinueConversation); } - public static void CreateDialog(string text, Character speaker, IEnumerable options, int[] closingOptions, string eventSprite, UInt16 actionId, bool fadeToBlack, DialogTypes dialogType, bool continueConversation = false) + public static void CreateDialog(LocalizedString text, Character speaker, IEnumerable options, int[] closingOptions, string eventSprite, UInt16 actionId, bool fadeToBlack, DialogTypes dialogType, bool continueConversation = false) { CreateDialog(text, speaker, options, closingOptions, actionInstance: null, actionId: actionId, spriteIdentifier: eventSprite, fadeToBlack: fadeToBlack, dialogType: dialogType, continueConversation: continueConversation); } - private static void CreateDialog(string text, Character speaker, IEnumerable options, int[] closingOptions, string spriteIdentifier = null, + private static void CreateDialog(LocalizedString text, Character speaker, IEnumerable options, int[] closingOptions, string spriteIdentifier = null, ConversationAction actionInstance = null, UInt16? actionId = null, bool fadeToBlack = false, DialogTypes dialogType = DialogTypes.Regular, bool continueConversation = false) { Debug.Assert(actionInstance == null || actionId == null); @@ -126,6 +126,7 @@ namespace Barotrauma if (actionInstance != null) { lastActiveAction = actionInstance; + actionInstance.lastActiveTime = Timing.TotalTime; actionInstance.dialogBox = messageBox; } else @@ -313,7 +314,7 @@ namespace Barotrauma }; } - private static List CreateConversation(GUIListBox parentBox, string text, Character speaker, IEnumerable options, bool drawChathead = true) + private static List CreateConversation(GUIListBox parentBox, LocalizedString text, Character speaker, IEnumerable options, bool drawChathead = true) { var content = new GUILayoutGroup(new RectTransform(Vector2.One, parentBox.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) { @@ -322,9 +323,11 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = speaker?.DisplayName is not null ? - TextManager.GetWithVariable(text, "[speakername]", speaker?.DisplayName) : - TextManager.Get(text); + LocalizedString translatedText = text.Replace("\\n", "\n"); + if (speaker?.DisplayName is not null) + { + translatedText = translatedText.Replace("[speakername]", speaker.DisplayName); + } translatedText = TextManager.ParseInputTypes(translatedText).Fallback(text); if (speaker?.Info != null && drawChathead) @@ -341,7 +344,7 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(5) }; - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), translatedText, wrap: true) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), RichString.Rich(translatedText), wrap: true) { AlwaysOverrideCursor = true, UserData = "text" diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs new file mode 100644 index 000000000..7be50d745 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Barotrauma; + +partial class EventLogAction : EventAction +{ + partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) + { + eventLog?.AddEntry(ParentEvent.Prefab.Identifier, Id, displayText); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs new file mode 100644 index 000000000..2686bd1af --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs @@ -0,0 +1,66 @@ +#nullable enable + +namespace Barotrauma; + +partial class EventObjectiveAction : EventAction +{ + public static void Trigger( + SegmentActionType Type, + Identifier Identifier, + Identifier ObjectiveTag, + Identifier ParentObjectiveId, + Identifier TextTag, + bool CanBeCompleted, + bool autoPlayVideo = false, + string videoFile = "", + int width = 450, + int height = 80) + { + 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) + { + segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, autoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new ObjectiveManager.Segment.Text(TextTag, width, height, Anchor.Center), + new ObjectiveManager.Segment.Video(videoFile, TextTag, width, height)); + } + else if (Type == SegmentActionType.Add) + { + segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); + } + if (segment is not null) + { + segment.CanBeCompleted = CanBeCompleted; + segment.ParentId = ParentObjectiveId; + } + switch (Type) + { + case SegmentActionType.Trigger: + case SegmentActionType.Add: + ObjectiveManager.TriggerSegment(segment); + break; + case SegmentActionType.Complete: + ObjectiveManager.CompleteSegment(Identifier); + break; + case SegmentActionType.Remove: + ObjectiveManager.RemoveSegment(Identifier); + break; + case SegmentActionType.CompleteAndRemove: + ObjectiveManager.CompleteSegment(Identifier); + ObjectiveManager.RemoveSegment(Identifier); + break; + case SegmentActionType.Fail: + ObjectiveManager.FailSegment(Identifier); + break; + case SegmentActionType.FailAndRemove: + ObjectiveManager.FailSegment(Identifier); + ObjectiveManager.RemoveSegment(Identifier); + break; + } + } + + partial void UpdateProjSpecific() + { + Trigger(Type, Identifier, ObjectiveTag, ParentObjectiveId, TextTag, CanBeCompleted, AutoPlayVideo, VideoFile, Width, Height); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index 2cf27ab55..0929a180f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -17,7 +17,7 @@ partial class MessageBoxAction : EventAction var segment = ObjectiveManager.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); segment.CanBeCompleted = ObjectiveCanBeCompleted; segment.ParentId = ParentObjectiveId; - ObjectiveManager.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); + ObjectiveManager.TriggerSegment(segment, connectObjective: Type == ActionType.ConnectObjective); } } else if (Type == ActionType.Close) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs deleted file mode 100644 index 6420009e8..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Barotrauma; - -partial class TutorialSegmentAction : EventAction -{ - private ObjectiveManager.Segment segment; - - partial void UpdateProjSpecific() - { - // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) - if (Type == SegmentActionType.Trigger) - { - segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, - new ObjectiveManager.Segment.Text(TextTag, Width, Height, Anchor.Center), - new ObjectiveManager.Segment.Video(VideoFile, TextTag, Width, Height)); - } - else if (Type == SegmentActionType.Add) - { - segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); - } - if (segment is not null) - { - segment.CanBeCompleted = CanBeCompleted; - segment.ParentId = ParentObjectiveId; - } - switch (Type) - { - case SegmentActionType.Trigger: - case SegmentActionType.Add: - ObjectiveManager.TriggerTutorialSegment(segment); - break; - case SegmentActionType.Complete: - ObjectiveManager.CompleteTutorialSegment(Identifier); - break; - case SegmentActionType.Remove: - ObjectiveManager.RemoveTutorialSegment(Identifier); - break; - case SegmentActionType.CompleteAndRemove: - ObjectiveManager.CompleteTutorialSegment(Identifier); - ObjectiveManager.RemoveTutorialSegment(Identifier); - break; - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs new file mode 100644 index 000000000..0db59ddea --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs @@ -0,0 +1,61 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class EventLog +{ + public bool UnreadEntries { get; private set; } + + public void AddEntry(Identifier eventPrefabId, Identifier entryId, string text) + { + TryAddEntryInternal(eventPrefabId, entryId, text); + GameMain.GameSession?.EnableEventLogNotificationIcon(enabled: true); + UnreadEntries = true; + } + + public void CreateEventLogUI(GUIComponent parent, TraitorManager.TraitorResults? traitorResults = null) + { + UnreadEntries = false; + + int spacing = GUI.IntScale(5); + foreach (var ev in events.Values) + { + LocalizedString nameString = string.Empty; + int difficultyIconCount = 0; + + EventPrefab.Prefabs.TryGet(ev.EventIdentifier, out EventPrefab? eventPrefab); + if (eventPrefab is not null) + { + nameString = RichString.Rich(eventPrefab.Name); + if (eventPrefab is TraitorEventPrefab traitorEventPrefab) + { + difficultyIconCount = traitorEventPrefab.DangerLevel; + } + } + var textContent = new List(); + textContent.AddRange(ev.Entries.Select(e => (LocalizedString)e.Text)); + + var icon = GUIStyle.GetComponentStyle("TraitorMissionIcon")?.GetDefaultSprite(); + + RoundSummary.CreateMissionEntry( + parent, + nameString, + textContent, + difficultyIconCount, + icon, GUIStyle.Red, + out GUIImage missionIcon); + + if (traitorResults != null && + traitorResults.Value.TraitorEventIdentifier == ev.EventIdentifier) + { + RoundSummary.UpdateMissionStateIcon(traitorResults.Value.ObjectiveSuccessful, missionIcon); + } + } + } +} + diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 1615092b8..27007040d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -413,23 +413,9 @@ namespace Barotrauma private Rectangle DrawScriptedEvent(SpriteBatch spriteBatch, ScriptedEvent scriptedEvent, Rectangle? parentRect = null) { - EventAction? currentEvent = !scriptedEvent.IsFinished ? scriptedEvent.Actions[scriptedEvent.CurrentActionIndex] : null; - List positions = new List(); - string text = $"Finished: {scriptedEvent.IsFinished.ColorizeObject()}\n" + - $"Action index: {scriptedEvent.CurrentActionIndex.ColorizeObject()}\n" + - $"Current action: {currentEvent?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n"; - - text += "All actions:\n"; - text += FindActions(scriptedEvent).Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.Item1 * 6)}{action.Item2.ToDebugString()}\n"); - - text += "Targets:\n"; - foreach (var (key, value) in scriptedEvent.Targets) - { - text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n"; - } - + string text = scriptedEvent.GetDebugInfo(); if (scriptedEvent.Targets != null) { foreach ((_, List entities) in scriptedEvent.Targets) @@ -452,10 +438,7 @@ namespace Barotrauma { debugPositions.Clear(); - string text = $"Finished: {artifactEvent.IsFinished.ColorizeObject()}\n" + - $"Item: {artifactEvent.Item.ColorizeObject()}\n" + - $"Spawn pending: {artifactEvent.SpawnPending.ColorizeObject()}\n" + - $"Spawn position: {artifactEvent.SpawnPos.ColorizeObject()}\n"; + string text = artifactEvent.GetDebugInfo(); if (artifactEvent.Item != null && !artifactEvent.Item.Removed) { @@ -470,10 +453,7 @@ namespace Barotrauma { debugPositions.Clear(); - string text = $"Finished: {monsterEvent.IsFinished.ColorizeObject()}\n" + - $"Amount: {monsterEvent.MinAmount.ColorizeObject()} - {monsterEvent.MaxAmount.ColorizeObject()}\n" + - $"Spawn pending: {monsterEvent.SpawnPending.ColorizeObject()}\n" + - $"Spawn position: {monsterEvent.SpawnPos.ColorizeObject()}\n"; + string text = monsterEvent.GetDebugInfo(); if (monsterEvent.SpawnPos != null && Submarine.MainSub != null) { @@ -712,7 +692,30 @@ namespace Barotrauma } } break; + case NetworkEventType.EVENTLOG: + ClientReadEventLog(GameMain.Client, msg); + break; + case NetworkEventType.EVENTOBJECTIVE: + ClientReadEventObjective(GameMain.Client, msg); + break; } } + + private void ClientReadEventLog(GameClient client, IReadMessage msg) + { + NetEventLogEntry entry = INetSerializableStruct.Read(msg); + EventLog.AddEntry(entry.EventPrefabId, entry.LogEntryId, entry.Text.Replace("\\n", "\n")); + } + private static void ClientReadEventObjective(GameClient client, IReadMessage msg) + { + NetEventObjective entry = INetSerializableStruct.Read(msg); + EventObjectiveAction.Trigger( + entry.Type, + entry.Identifier, + entry.ObjectiveTag, + entry.ParentObjectiveId, + entry.TextTag, + entry.CanBeCompleted); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index 41a7758b3..f0e471346 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using System.Collections.Generic; @@ -9,22 +10,37 @@ namespace Barotrauma public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; + public override int State + { + get => base.State; + set + { + base.State = value; + if (base.State > 0) + { + caves.ForEach(c => c.MissionsToDisplayOnSonar.Remove(this)); + } + } + } + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); byte caveCount = msg.ReadByte(); for (int i = 0; i < caveCount; i++) { - byte selectedCave = msg.ReadByte(); - if (selectedCave < 255 && Level.Loaded != null) + byte selectedCaveIndex = msg.ReadByte(); + if (selectedCaveIndex < 255 && Level.Loaded != null) { - if (selectedCave < Level.Loaded.Caves.Count) + if (selectedCaveIndex < Level.Loaded.Caves.Count) { - Level.Loaded.Caves[selectedCave].DisplayOnSonar = true; + var selectedCave = Level.Loaded.Caves[selectedCaveIndex]; + selectedCave.MissionsToDisplayOnSonar.Add(this); + caves.Add(selectedCave); } else { - DebugConsole.ThrowError($"Cave index out of bounds when reading nest mission data. Index: {selectedCave}, number of caves: {Level.Loaded.Caves.Count}"); + DebugConsole.ThrowError($"Cave index out of bounds when reading nest mission data. Index: {selectedCaveIndex}, number of caves: {Level.Loaded.Caves.Count}"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 4a0e51172..fdf6ccfa8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using static Barotrauma.MissionPrefab; namespace Barotrauma { @@ -27,7 +28,11 @@ namespace Barotrauma public Color GetDifficultyColor() { - int v = Difficulty ?? MissionPrefab.MinDifficulty; + return GetDifficultyColor(Difficulty ?? MissionPrefab.MinDifficulty); + } + public static Color GetDifficultyColor(int difficulty) + { + int v = difficulty; float t = MathUtils.InverseLerp(MissionPrefab.MinDifficulty, MissionPrefab.MaxDifficulty, v); return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } @@ -61,36 +66,48 @@ namespace Barotrauma List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - FactionPrefab targetFactionPrefab; - if (reputationReward.Key == "location" ) + FactionPrefab factionPrefab; + if (reputationReward.FactionIdentifier == "location" ) { - targetFactionPrefab = OriginLocation.Faction?.Prefab; + factionPrefab = OriginLocation.Faction?.Prefab; } else { - FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFactionPrefab); - } - - if (targetFactionPrefab == null) - { - return string.Empty; + FactionPrefab.Prefabs.TryGet(reputationReward.FactionIdentifier, out factionPrefab); } - float totalReputationChange = reputationReward.Value; - if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == targetFactionPrefab) is Faction faction) + if (factionPrefab != null) { - totalReputationChange = reputationReward.Value * faction.Reputation.GetReputationChangeMultiplier(reputationReward.Value); + AddReputationText(factionPrefab, reputationReward.Amount); + if (!MathUtils.NearlyEqual(reputationReward.AmountForOpposingFaction, 0.0f) && + FactionPrefab.Prefabs.TryGet(factionPrefab.OpposingFaction, out var opposingFactionPrefab)) + { + AddReputationText(opposingFactionPrefab, reputationReward.AmountForOpposingFaction); + } + } + } + + void AddReputationText(FactionPrefab factionPrefab, float amount) + { + if (factionPrefab == null) { return; } + + float totalReputationChange = amount; + if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == factionPrefab) is Faction faction) + { + totalReputationChange = amount * faction.Reputation.GetReputationChangeMultiplier(amount); } - LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(targetFactionPrefab.IconColor)}‖{targetFactionPrefab.Name}‖end‖"; + LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(factionPrefab.IconColor)}‖{factionPrefab.Name}‖end‖"; float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, totalReputationChange); string formattedValue = ((int)Math.Round(totalReputationChange)).ToString("+#;-#;0"); //force plus sign for positive numbers LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", ("[reputationname]", name), - ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖")); reputationRewardTexts.Add(rewardText.Value); } + + if (reputationRewardTexts.Any()) { return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); @@ -100,6 +117,20 @@ namespace Barotrauma return string.Empty; } } + partial void DistributeExperienceToCrew(IEnumerable crew, int experienceGain) + { + foreach (Character character in crew) + { + GiveMissionExperience(character.Info); + } + void GiveMissionExperience(CharacterInfo info) + { + if (info == null) { return; } + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + info.GiveExperience((int)(experienceGain * experienceGainMultiplierIndividual.Value)); + } + } partial void ShowMessageProjSpecific(int missionState) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs index f9538a0a3..ff85bdc8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs @@ -9,6 +9,19 @@ namespace Barotrauma public override bool DisplayAsCompleted => State > 0 && !requireDelivery; public override bool DisplayAsFailed => false; + public override int State + { + get => base.State; + set + { + base.State = value; + if (base.State > 0 && selectedCave != null) + { + selectedCave.MissionsToDisplayOnSonar.Remove(this); + } + } + } + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); @@ -20,8 +33,9 @@ namespace Barotrauma { if (selectedCaveIndex < Level.Loaded.Caves.Count) { - Level.Loaded.Caves[selectedCaveIndex].DisplayOnSonar = true; - SpawnNestObjects(Level.Loaded, Level.Loaded.Caves[selectedCaveIndex]); + selectedCave = Level.Loaded.Caves[selectedCaveIndex]; + selectedCave.MissionsToDisplayOnSonar.Add(this); + SpawnNestObjects(Level.Loaded, selectedCave); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 15860e012..4afb04ff2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -99,10 +99,10 @@ namespace Barotrauma })) .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category); - public ScalableFont(ContentXElement element, GraphicsDevice gd = null) + public ScalableFont(ContentXElement element, uint defaultSize = 14, GraphicsDevice gd = null) : this( element.GetAttributeContentPath("file")?.Value, - (uint)element.GetAttributeInt("size", 14), + (uint)element.GetAttributeInt("size", (int)defaultSize), gd, element.GetAttributeBool("dynamicloading", false), ExtractShccFromXElement(element)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 2456e2ba8..7f185b4d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -38,6 +38,7 @@ namespace Barotrauma private float prevUIScale; private readonly GUIFrame channelSettingsFrame; + private readonly GUITextBlock radioJammedWarning; private readonly GUITextBox channelText; private readonly GUILayoutGroup channelPickerContent; private readonly GUIButton memButton; @@ -107,6 +108,13 @@ namespace Barotrauma RelativeSpacing = 0.01f }; + radioJammedWarning = new GUITextBlock(new RectTransform(Vector2.One, channelSettingsFrame.RectTransform), TextManager.Get("radiojammedwarning"), + textColor: GUIStyle.Orange, color: Color.Black, + textAlignment: Alignment.Center, style: "OuterGlow") + { + ToolTip = TextManager.Get("hint.radiojammed") + }; + var buttonLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { PlaySoundOnSelect = false, @@ -643,7 +651,7 @@ namespace Barotrauma ToggleButton.RectTransform.AbsoluteOffset = new Point(GUIFrame.Rect.Right, GUIFrame.Rect.Y + HUDLayoutSettings.ChatBoxArea.Height - ToggleButton.Rect.Height); } - if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio, ignoreJamming: true)) { if (prevRadio != radio) { @@ -672,10 +680,11 @@ namespace Barotrauma } } channelSettingsFrame.Visible = true; + radioJammedWarning.Visible = radio is { JamTimer: > 0 }; } else { - channelSettingsFrame.Visible = false; + radioJammedWarning.Visible = channelSettingsFrame.Visible = false; channelPickerContent.Children.First().CanBeFocused = true; channelMemPending = false; memButton.Enabled = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index f460ff480..c04c21183 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -756,7 +756,7 @@ namespace Barotrauma private bool CreateRenamingComponent(GUIButton button, object userData) { - if (!HasPermission || !(userData is CharacterInfo characterInfo)) { return false; } + if (!HasPermission || userData is not CharacterInfo characterInfo) { return false; } var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f); var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center) @@ -843,7 +843,7 @@ namespace Barotrauma private bool FireCharacter(GUIButton button, object selection) { - if (!(selection is CharacterInfo characterInfo)) { return false; } + if (selection is not CharacterInfo characterInfo) { return false; } campaign.CrewManager.FireCharacter(characterInfo); SelectCharacter(null, null, null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 373fecbbf..d0b10b1e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -161,7 +161,7 @@ namespace Barotrauma return 1; } - return string.Compare(file1, file2); + return string.Compare(file1, file2, StringComparison.OrdinalIgnoreCase); } private static void InitIfNecessary() @@ -419,6 +419,8 @@ namespace Barotrauma }; } + fileList.Content.RectTransform.SortChildren(SortFiles); + directoryBox!.Text = currentDirectory; fileBox!.Text = ""; fileList.Deselect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 838c2ee27..3a0bff6d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -111,7 +111,7 @@ namespace Barotrauma /// public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; - public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.3f; public static int UIWidth { @@ -1010,16 +1010,13 @@ namespace Barotrauma // Sub editor drag and highlight case SubEditorScreen editor: { - foreach (var mapEntity in MapEntity.mapEntityList) + if (MapEntity.StartMovingPos != Vector2.Zero || MapEntity.Resizing) { - if (MapEntity.StartMovingPos != Vector2.Zero) - { - return CursorState.Dragging; - } - if (mapEntity.IsHighlighted) - { - return CursorState.Hand; - } + return CursorState.Dragging; + } + if (MapEntity.HighlightedEntities.Any(h => !h.IsSelected)) + { + return CursorState.Hand; } break; } @@ -2438,13 +2435,14 @@ namespace Barotrauma var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) }); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.6f), pauseMenuInner.RectTransform, Anchor.Center)) + float padding = 0.06f; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.8f), pauseMenuInner.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, padding) }) { - Stretch = true, - RelativeSpacing = 0.05f + AbsoluteSpacing = IntScale(15) }; - new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, + new GUIButton(new RectTransform(new Vector2(0.1f, 0.07f), pauseMenuInner.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(padding) }, "", style: "GUIBugButton") { IgnoreLayoutGroups = true, @@ -2520,6 +2518,13 @@ namespace Barotrauma } GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); + //scale to ensure there's enough room for all the buttons + pauseMenuInner.RectTransform.MinSize = new Point( + pauseMenuInner.RectTransform.MinSize.X, + Math.Max( + (int)(buttonContainer.Children.Sum(c => c.Rect.Height + buttonContainer.AbsoluteSpacing) / buttonContainer.RectTransform.RelativeSize.Y), + pauseMenuInner.RectTransform.MinSize.X)); + } void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index b6460f83f..e3ea42eaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -781,7 +781,7 @@ namespace Barotrauma if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) { - toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0); + toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width + targetElement.Width, 0); } if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs index 2d549b630..72a349ae2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs @@ -49,7 +49,7 @@ namespace Barotrauma get { return lifeTime; } } - public ScalableFont Font + public GUIFont Font { get; private set; @@ -69,7 +69,7 @@ namespace Barotrauma } } - public GUIMessage(string text, Color color, float lifeTime, ScalableFont font = null) + public GUIMessage(string text, Color color, float lifeTime, GUIFont font = null) { coloredText = new ColoredText(text, color, false, false); this.lifeTime = lifeTime; @@ -81,7 +81,7 @@ namespace Barotrauma Font = font; } - public GUIMessage(string text, Color color, Vector2 position, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, ScalableFont font = null, Submarine sub = null) + public GUIMessage(string text, Color color, Vector2 position, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, GUIFont font = null, Submarine sub = null) { coloredText = new ColoredText(text, color, false, false); WorldSpace = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 4d3f69ea8..c5e6fda31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -693,5 +693,52 @@ namespace Barotrauma rectT.Parent = RectTransform; Buttons.Add(new GUIButton(rectT, text) { OnClicked = onClick }); } + + public static GUIMessageBox CreateLoadingBox(LocalizedString text, (LocalizedString Label, Action Action)[] buttons = null, Vector2? relativeSize = null) + { + buttons ??= Array.Empty<(LocalizedString Label, Action Action)>(); + var relativeSizeFallback = relativeSize ?? (0.7f, 0.5f); + var newMessageBox = new GUIMessageBox( + headerText: "", + text: "", + relativeSize: relativeSizeFallback, + buttons: buttons.Select(b => b.Label).ToArray()); + newMessageBox.InnerFrame.RectTransform.ScaleBasis = ScaleBasis.BothHeight; + + for (int i = 0; i < buttons.Length; i++) + { + var capturedIndex = i; + newMessageBox.Buttons[i].OnClicked = (_, _) => + { + buttons[capturedIndex].Action(newMessageBox); + return false; + }; + } + + const float throbberSize = 0.25f; + + new GUITextBlock( + new RectTransform((0.9f, 0f), newMessageBox.InnerFrame.RectTransform, Anchor.Center, Pivot.BottomCenter) { RelativeOffset = (0f, -throbberSize * 0.5f) }, + text: text, textAlignment: Alignment.Center, wrap: true); + + // Throbber + new GUICustomComponent( + new RectTransform(Vector2.One * throbberSize, newMessageBox.InnerFrame.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.BothHeight), + onDraw: static (sb, component) => + { + GUIStyle.GenericThrobber.Draw( + sb, + spriteIndex: (int)(Timing.TotalTime * 20f) % GUIStyle.GenericThrobber.FrameCount, + pos: component.Rect.Center.ToVector2(), + color: Color.White, + origin: GUIStyle.GenericThrobber.FrameSize.ToVector2() * 0.5f, + rotate: 0f, + scale: component.Rect.Size.ToVector2() / GUIStyle.GenericThrobber.FrameSize.ToVector2()); + }); + + MessageBoxes.Remove(newMessageBox); + MessageBoxes.Insert(0, newMessageBox); + return newMessageBox; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index ee15c0c06..8d77932bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -132,7 +132,7 @@ namespace Barotrauma if (subElement.NameAsIdentifier() != "override") { continue; } if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag)) { - return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); + return new ScalableFont(subElement, font.Size, GameMain.Instance.GraphicsDevice); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs index 3969a3db3..459447cd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - public class GUIScissorComponent: GUIComponent + public sealed class GUIScissorComponent : GUIComponent { public GUIComponent Content; @@ -14,19 +14,14 @@ namespace Barotrauma { CanBeFocused = false }; + + rectT.ChildrenChanged += CheckForChildren; } - protected override void Update(float deltaTime) + private void CheckForChildren(RectTransform rectT) { - base.Update(deltaTime); - - foreach (GUIComponent child in Children) - { - if (child == Content) { continue; } - throw new InvalidOperationException($"Children were found in {nameof(GUIScissorComponent)}, Add them to {nameof(GUIScissorComponent)}.{nameof(Content)} instead."); - } - - ClampChildMouseRects(Content); + if (rectT == Content.RectTransform) { return; } + throw new InvalidOperationException($"Children were found in {nameof(GUIScissorComponent)}, Add them to {nameof(GUIScissorComponent)}.{nameof(Content)} instead."); } public override void DrawChildren(SpriteBatch spriteBatch, bool recursive) @@ -57,7 +52,13 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: prevRasterizerState); } - private void ClampChildMouseRects(GUIComponent child) + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + ClampChildMouseRects(Content); + } + + private static void ClampChildMouseRects(GUIComponent child) { child.ClampMouseRectToParent = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index e3ad34ad7..05d7b129c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -142,11 +142,13 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned"); + private readonly static Point defaultItemFrameMargin = new Point(50, 56); + public static Point ItemFrameMargin { get { - Point size = new Point(50, 56).Multiply(GUI.SlicedSpriteScale); + Point size = defaultItemFrameMargin.Multiply(GUI.SlicedSpriteScale); var style = GetComponentStyle("ItemUI"); var sprite = style?.Sprites[GUIComponent.ComponentState.None].First(); @@ -159,6 +161,16 @@ namespace Barotrauma } } + public static int ItemFrameTopBarHeight + { + get + { + var style = GetComponentStyle("ItemUI"); + var sprite = style?.Sprites[GUIComponent.ComponentState.None].First(); + return (int)Math.Min(sprite?.Slices[0].Height ?? 0, defaultItemFrameMargin.Y / 2 * GUI.SlicedSpriteScale); + } + } + public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); public static GUIComponentStyle GetComponentStyle(string styleName) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 8ab3992d5..cc7359b59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -12,8 +12,6 @@ namespace Barotrauma public delegate bool OnSelectedHandler(GUITickBox obj); public OnSelectedHandler OnSelected; - public static int size = 20; - private GUIRadioButtonGroup radioButtonGroup; public override bool Selected @@ -21,21 +19,7 @@ namespace Barotrauma get { return isSelected; } set { - if (value == isSelected) { return; } - if (radioButtonGroup != null && radioButtonGroup.SelectedRadioButton == this) - { - isSelected = true; - return; - } - - isSelected = value; - State = isSelected ? ComponentState.Selected : ComponentState.None; - if (value && radioButtonGroup != null) - { - radioButtonGroup.SelectRadioButton(this); - } - - OnSelected?.Invoke(this); + SetSelected(value, callOnSelected: true); } } @@ -186,6 +170,27 @@ namespace Barotrauma text.SetTextPos(); ContentWidth = box.Rect.Width + text.Padding.X + text.TextSize.X + text.Padding.Z; } + + public void SetSelected(bool selected, bool callOnSelected = true) + { + if (selected == isSelected) { return; } + if (radioButtonGroup != null && radioButtonGroup.SelectedRadioButton == this) + { + isSelected = true; + return; + } + + isSelected = selected; + State = isSelected ? ComponentState.Selected : ComponentState.None; + if (selected && radioButtonGroup != null) + { + radioButtonGroup.SelectRadioButton(this); + } + if (callOnSelected) + { + OnSelected?.Invoke(this); + } + } protected override void Update(float deltaTime) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 8442731b2..f6bb9831e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -160,8 +160,9 @@ namespace Barotrauma int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; - float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f; - CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight); + CrewArea = new Rectangle(Padding, crewAreaY, + (int)MathHelper.Clamp(400 * GUI.Scale, 220, GameMain.GraphicsHeight * 0.4f), + crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 0c5c6cc17..52749f42b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -4,12 +4,13 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace Barotrauma { - class LoadingScreen + sealed class LoadingScreen { private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; @@ -37,36 +38,16 @@ namespace Barotrauma } } - private Queue pendingSplashScreens = new Queue(); /// /// Triplet.first = filepath, Triplet.second = resolution, Triplet.third = audio gain /// - public Queue PendingSplashScreens - { - get - { - lock (loadMutex) - { - return pendingSplashScreens; - } - } - set - { - lock (loadMutex) - { - pendingSplashScreens = value; - } - } - } + public readonly ConcurrentQueue PendingSplashScreens = new ConcurrentQueue(); public bool PlayingSplashScreen { get { - lock (loadMutex) - { - return currSplashScreen != null || pendingSplashScreens.Count > 0; - } + return currSplashScreen != null || PendingSplashScreens.Count > 0; } } @@ -76,33 +57,7 @@ namespace Barotrauma selectedTip = RichString.Rich(tip); } - private readonly object loadMutex = new object(); - private float? loadState; - - public float? LoadState - { - get - { - lock (loadMutex) - { - return loadState; - } - } - set - { - lock (loadMutex) - { - loadState = value; - DrawLoadingText = true; - } - } - } - - public bool DrawLoadingText - { - get; - set; - } + public float LoadState; public bool WaitForLanguageSelection { @@ -121,7 +76,7 @@ namespace Barotrauma overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); - DrawLoadingText = true; + SetSelectedTip(TextManager.Get("LoadingScreenTip")); } @@ -170,10 +125,11 @@ namespace Barotrauma { DrawLanguageSelectionPrompt(spriteBatch, graphics); } - else if (DrawLoadingText) + else { LocalizedString loadText; - if (LoadState == 100.0f) + var loadState = LoadState; // avoid multiple reads here to prevent jank + if (loadState >= 100.0f) { #if DEBUG if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || (GameSettings.CurrentConfig.TestScreenEnabled && GameMain.FirstLoad)) @@ -191,17 +147,17 @@ namespace Barotrauma else { loadText = TextManager.Get("Loading"); - if (LoadState != null) + if (loadState >= 0f) { - loadText += " " + (int)LoadState + " %"; + loadText += $" {loadState:N0} %"; + } #if DEBUG - if (GameMain.FirstLoad && GameMain.CancelQuickStart) - { - loadText += " (Quickstart aborted)"; - } -#endif + if (GameMain.FirstLoad && GameMain.CancelQuickStart) + { + loadText += " (Quickstart aborted)"; } +#endif } if (GUIStyle.LargeFont.HasValue) @@ -343,11 +299,9 @@ namespace Barotrauma private void DrawSplashScreen(SpriteBatch spriteBatch, GraphicsDevice graphics) { - if (currSplashScreen == null && PendingSplashScreens.Count == 0) { return; } - if (currSplashScreen == null) { - var newSplashScreen = PendingSplashScreens.Dequeue(); + if (!PendingSplashScreens.TryDequeue(out var newSplashScreen)) { return; } string fileName = newSplashScreen.Filename; try { @@ -362,10 +316,10 @@ namespace Barotrauma PendingSplashScreens.Clear(); currSplashScreen = null; } - - if (currSplashScreen == null) { return; } } + if (currSplashScreen == null) { return; } + if (currSplashScreen.IsPlaying) { graphics.Clear(Color.Black); @@ -425,7 +379,7 @@ namespace Barotrauma public IEnumerable DoLoading(IEnumerable loader) { drawn = false; - LoadState = null; + LoadState = -1f; SetSelectedTip(TextManager.Get("LoadingScreenTip")); currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index e19738609..85fe74086 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -946,7 +946,7 @@ namespace Barotrauma } if (truncated) { - descriptionBlock.Text += "..."; + descriptionBlock.Text += TextManager.Get("ellipsis"); } GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.25f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.SubHeadingFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 2e5826656..e7902d5ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -306,6 +306,8 @@ namespace Barotrauma { _scaleBasis = value; RecalculateAbsoluteSize(); + RecalculateAnchorPoint(); + RecalculatePivotOffset(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs index 3648f41af..71461fd0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs @@ -91,8 +91,8 @@ namespace Barotrauma var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); var scale = new Vector2(length, thickness); Vector2 middle = new Vector2((point1.X + point2.X) / 2f, (point1.Y + point2.Y) / 2f); - Texture2D tex = GetTexture(spriteBatch); - spriteBatch.Draw(GetTexture(spriteBatch), middle, null, color, angle, new Vector2(tex.Width / 2f, tex.Height / 2f), scale, SpriteEffects.None, 0); + Texture2D tex = GUI.WhiteTexture; + spriteBatch.Draw(tex, middle, null, color, angle, new Vector2(tex.Width / 2f, tex.Height / 2f), scale, SpriteEffects.None, 0); } private static void DrawPolygonEdge(SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness) @@ -112,6 +112,18 @@ namespace Barotrauma DrawLine(spriteBatch, new Vector2(x1, y1), new Vector2(x2, y2), color, thickness); } + public static void DrawLineWithTexture(this SpriteBatch spriteBatch, Texture2D tex, Vector2 point1, Vector2 point2, + Color color, float thickness = 1f) + { + // calculate the distance between the two vectors + var distance = Vector2.Distance(point1, point2); + + // calculate the angle between the two vectors + var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); + + DrawLine(spriteBatch, tex, point1, distance, angle, color, thickness); + } + /// /// Draws a line from point1 to point2 with an offset /// @@ -124,18 +136,18 @@ namespace Barotrauma // calculate the angle between the two vectors var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); - DrawLine(spriteBatch, point1, distance, angle, color, thickness); + DrawLine(spriteBatch, GetTexture(spriteBatch), point1, distance, angle, color, thickness); } /// /// Draws a line from point1 to point2 with an offset /// - public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point, float length, float angle, Color color, + public static void DrawLine(this SpriteBatch spriteBatch, Texture2D tex, Vector2 point, float length, float angle, Color color, float thickness = 1f) { - var origin = new Vector2(0f, 0.5f); - var scale = new Vector2(length, thickness); - spriteBatch.Draw(GetTexture(spriteBatch), point, null, color, angle, origin, scale, SpriteEffects.None, 0); + var origin = new Vector2(0f, tex.Height / 2f); + var scale = new Vector2(length / tex.Width, thickness / tex.Height); + spriteBatch.Draw(tex, point, null, color, angle, origin, scale, SpriteEffects.None, 0); } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 6b8ccfbb8..24362e63c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -19,7 +19,7 @@ namespace Barotrauma private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; - public enum InfoFrameTab { Crew, Mission, Reputation, Traitor, Submarine, Talents }; + public enum InfoFrameTab { Crew, Mission, Reputation, Submarine, Talents }; public static InfoFrameTab SelectedTab { get; private set; } private GUIFrame infoFrame, contentFrame; @@ -299,9 +299,15 @@ namespace Barotrauma var crewButton = createTabButton(InfoFrameTab.Crew, "crew"); - if (!(GameMain.GameSession?.GameMode is TestGameMode)) + if (GameMain.GameSession?.GameMode is not TestGameMode) { - createTabButton(InfoFrameTab.Mission, "mission"); + var missionBtn = createTabButton(InfoFrameTab.Mission, "mission"); + eventLogNotification = GameSession.CreateNotificationIcon(missionBtn); + eventLogNotification.Visible = GameMain.GameSession.EventManager?.EventLog?.UnreadEntries ?? false; + if (eventLogNotification.Visible) + { + eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); + } } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) @@ -340,14 +346,6 @@ namespace Barotrauma text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance)); } } - else - { - bool isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; - if (isTraitor && GameMain.Client.TraitorMission != null) - { - var traitorButton = createTabButton(InfoFrameTab.Traitor, "tabmenu.traitor"); - } - } var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); @@ -361,7 +359,7 @@ namespace Barotrauma } }; - talentPointNotification = GameSession.CreateTalentIconNotification(talentsButton); + talentPointNotification = GameSession.CreateNotificationIcon(talentsButton); } public void SelectInfoFrameTab(InfoFrameTab selectedTab) @@ -387,12 +385,6 @@ namespace Barotrauma GameMain.GameSession.RoundSummary.CreateReputationInfoPanel(reputationFrame, campaignMode); } break; - case InfoFrameTab.Traitor: - TraitorMissionPrefab traitorMission = GameMain.Client?.TraitorMission; - Character traitor = GameMain.Client?.Character; - if (traitor == null || traitorMission == null) { return; } - CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); - break; case InfoFrameTab.Submarine: CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; @@ -1539,96 +1531,44 @@ namespace Barotrauma int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; - GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); missionList.ContentBackground.Color = Color.Transparent; missionList.Spacing = GUI.IntScale(15); if (GameMain.GameSession?.Missions != null) { - int spacing = GUI.IntScale(5); - int iconSize = (int)(GUIStyle.LargeFont.MeasureChar('T').Y + GUIStyle.Font.MeasureChar('T').Y * 4 + spacing * 4); - foreach (Mission mission in GameMain.GameSession.Missions) { if (!mission.Prefab.ShowInMenus) { continue; } - GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) + + var textContent = new List() { - AbsoluteSpacing = spacing + mission.GetMissionRewardText(Submarine.MainSub), + mission.GetReputationRewardText(), + mission.Description }; - LocalizedString descriptionText = mission.Description; - foreach (LocalizedString missionMessage in mission.ShownMessages) + textContent.AddRange(mission.ShownMessages); + + RoundSummary.CreateMissionEntry( + missionList.Content, + mission.Name, + textContent, + mission.Difficulty ?? 0, + mission.Prefab.Icon, mission.Prefab.IconColor, + out GUIImage missionIcon); + if (missionIcon != null) { - descriptionText += "\n\n" + missionMessage; - } - RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); - RichString reputationText = mission.GetReputationRewardText(); + UpdateMissionStateIcon(); + mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(); - Func wrapMissionText(GUIFont font) - { - return (str) => ToolBox.WrapText(str, missionTextGroup.Rect.Width, font.Value); - } - RichString missionNameString = RichString.Rich(mission.Name, wrapMissionText(GUIStyle.LargeFont)); - RichString missionRewardString = RichString.Rich(rewardText, wrapMissionText(GUIStyle.Font)); - RichString missionReputationString = RichString.Rich(reputationText, wrapMissionText(GUIStyle.Font)); - RichString missionDescriptionString = RichString.Rich(descriptionText, wrapMissionText(GUIStyle.Font)); - - Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString.SanitizedValue); - Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString.SanitizedValue); - Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString.SanitizedValue); - Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString.SanitizedValue); - - float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; - bool displayDifficulty = mission.Difficulty.HasValue; - if (displayDifficulty) { ySize += missionRewardSize.Y; } - - missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)ySize); - missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - - if (mission.Prefab.Icon != null) - { - /*float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; - int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); - int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight);*/ - - var icon = new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) + void UpdateMissionStateIcon() { - Color = mission.Prefab.IconColor, - HoverColor = mission.Prefab.IconColor, - SelectedColor = mission.Prefab.IconColor, - CanBeFocused = false - }; - UpdateMissionStateIcon(mission, icon); - mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(mission, icon); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); - GUILayoutGroup difficultyIndicatorGroup = null; - if (displayDifficulty) - { - difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, (int)missionRewardSize.Y), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - AbsoluteSpacing = 1 - }; - var difficultyColor = mission.GetDifficultyColor(); - for (int i = 0; i < mission.Difficulty.Value; i++) - { - new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) + if (mission.DisplayAsCompleted || mission.DisplayAsFailed) { - CanBeFocused = false, - Color = difficultyColor - }; + RoundSummary.UpdateMissionStateIcon(mission.DisplayAsCompleted, missionIcon); + } } } - var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); - if (difficultyIndicatorGroup != null) - { - difficultyIndicatorGroup.RectTransform.Resize(new Point((int)(difficultyIndicatorGroup.Rect.Width - rewardTextBlock.Padding.X - rewardTextBlock.Padding.Z), difficultyIndicatorGroup.Rect.Height)); - difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)rewardTextBlock.Padding.X, 0); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationString); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } } else @@ -1636,66 +1576,14 @@ namespace Barotrauma GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUIStyle.LargeFont); } + + GameMain.GameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content); + GameMain.GameSession.EnableEventLogNotificationIcon(enabled: false); + + RoundSummary.AddSeparators(missionList.Content); } - private void UpdateMissionStateIcon(Mission mission, GUIImage missionIcon) - { - if (mission == null || missionIcon == null) { return; } - string style = string.Empty; - if (mission.DisplayAsFailed) - { - style = "MissionFailedIcon"; - } - else if (mission.DisplayAsCompleted) - { - style = "MissionCompletedIcon"; - } - GUIImage stateIcon = missionIcon.GetChild(); - if (string.IsNullOrEmpty(style)) - { - if (stateIcon != null) - { - stateIcon.Visible = false; - } - } - else - { - stateIcon ??= new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), style, scaleToFit: true); - stateIcon.Visible = true; - } - } - - private void CreateTraitorInfo(GUIFrame infoFrame, TraitorMissionPrefab traitorMission, Character traitor) - { - GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); - - int padding = (int)(0.0245f * missionFrame.Rect.Height); - - GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, style: null); - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.319f, 0f) }, false, childAnchor: Anchor.TopLeft); - - LocalizedString missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUIStyle.LargeFont); - LocalizedString missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUIStyle.Font); - - Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); - - missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y)); - missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - - float aspectRatio = traitorMission.Icon.SourceRect.Width / traitorMission.Icon.SourceRect.Height; - - int iconWidth = (int)(0.319f * missionDescriptionHolder.RectTransform.NonScaledSize.X); - int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * aspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight); - - new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), traitorMission.Icon, null, true) { Color = traitorMission.IconColor }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); - } - - private void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub) + private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub) { GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null); @@ -1793,7 +1681,7 @@ namespace Barotrauma } } - private GUIImage talentPointNotification; + private GUIImage talentPointNotification, eventLogNotification; public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index fc8dfd1f5..90c860332 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -117,6 +117,9 @@ namespace Barotrauma return MathHelper.Clamp(Math.Min(Math.Min(scale.X, scale.Y), GUI.SlicedSpriteScale), minBorderScale, maxBorderScale); } + public void Draw(SpriteBatch spriteBatch, RectangleF rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None, Vector2? uvOffset = null) + => Draw(spriteBatch, new Rectangle(rect.Location.ToPoint(), rect.Size.ToPoint()), color, spriteEffects, uvOffset); + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None, Vector2? uvOffset = null) { uvOffset ??= Vector2.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index a0a55d717..7ffcbfde4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -11,7 +11,7 @@ namespace Barotrauma { if (consentTextAvailable) { - var background = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + var background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: "GUIBackgroundBlocker"); var frame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), background.RectTransform, Anchor.Center) { MinSize = new Point(800, 0), MaxSize = new Point(1500, int.MaxValue) }); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), frame.RectTransform, Anchor.Center)) @@ -55,7 +55,8 @@ namespace Barotrauma yesBtn.OnClicked += (btn, userdata) => { GUIMessageBox.MessageBoxes.Remove(background); - SetConsentInternal(Consent.Yes); + var loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); + SetConsentInternal(Consent.Yes, onAnswerSent: loadingBox.Close); return true; }; yesBtn.Enabled = false; @@ -76,7 +77,8 @@ namespace Barotrauma noBtn.OnClicked += (btn, userdata) => { GUIMessageBox.MessageBoxes.Remove(background); - SetConsent(Consent.No); + var loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); + SetConsent(Consent.No, onAnswerSent: loadingBox.Close); return true; }; noBtn.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 8d8f7a838..4f4e5a284 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -111,7 +111,8 @@ namespace Barotrauma public static LoadingScreen TitleScreen; private bool loadingScreenOpen; - private CoroutineHandle loadingCoroutine; + private Thread initialLoadingThread; + public bool HasLoaded { get; private set; } private readonly GameTime fixedTime; @@ -411,24 +412,21 @@ namespace Barotrauma WaitForLanguageSelection = GameSettings.CurrentConfig.Language == LanguageIdentifier.None }; - bool canLoadInSeparateThread = true; - - loadingCoroutine = CoroutineManager.StartCoroutine(Load(canLoadInSeparateThread), "Load", canLoadInSeparateThread); + initialLoadingThread = new Thread(Load); + initialLoadingThread.Start(); } - public class LoadingException : Exception + private void Load() { - public LoadingException(Exception e) : base("Loading was interrupted due to an error.", innerException: e) + static void log(string str) { + if (GameSettings.CurrentConfig.VerboseLogging) + { + DebugConsole.NewMessage(str, Color.Lime); + } } - } - - private IEnumerable Load(bool isSeparateThread) - { - if (GameSettings.CurrentConfig.VerboseLogging) - { - DebugConsole.NewMessage("LOADING COROUTINE", Color.Lime); - } + + log("LOADING COROUTINE"); ContentPackageManager.LoadVanillaFileList(); @@ -438,7 +436,7 @@ namespace Barotrauma TitleScreen.AvailableLanguages = TextManager.AvailableLanguages.OrderBy(l => l.Value != "english".ToIdentifier()).ThenBy(l => l.Value).ToArray(); while (TitleScreen.WaitForLanguageSelection) { - yield return CoroutineStatus.Running; + Thread.Sleep((int)(Timing.Step * 1000)); } ContentPackageManager.VanillaCorePackage.UnloadFilesOfType(); } @@ -450,25 +448,13 @@ namespace Barotrauma { var pendingSplashScreens = TitleScreen.PendingSplashScreens; float baseVolume = MathHelper.Clamp(GameSettings.CurrentConfig.Audio.SoundVolume * 2.0f, 0.0f, 1.0f); - pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_UTG.webm", baseVolume * 0.5f)); - pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_FF.webm", baseVolume)); - pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_Daedalic.webm", baseVolume * 0.1f)); - } - - //if not loading in a separate thread, wait for the splash screens to finish before continuing the loading - //otherwise the videos will look extremely choppy - if (!isSeparateThread) - { - while (TitleScreen.PlayingSplashScreen || TitleScreen.PendingSplashScreens.Count > 0) - { - yield return CoroutineStatus.Running; - } + pendingSplashScreens.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_UTG.webm", baseVolume * 0.5f)); + pendingSplashScreens.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_FF.webm", baseVolume)); + pendingSplashScreens.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_Daedalic.webm", baseVolume * 0.1f)); } GUI.Init(); - yield return CoroutineStatus.Running; - LegacySteamUgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); foreach (var progress in contentPackageLoadRoutine @@ -476,7 +462,6 @@ namespace Barotrauma { const float min = 1f, max = 70f; TitleScreen.LoadState = MathHelper.Lerp(min, max, progress); - yield return CoroutineStatus.Running; } var corePackage = ContentPackageManager.EnabledPackages.Core; @@ -505,7 +490,6 @@ namespace Barotrauma TaskPool.Add("InitRelayNetworkAccess", SteamManager.InitRelayNetworkAccess(), (t) => { }); HintManager.Init(); - yield return CoroutineStatus.Running; CoreEntityPrefab.InitCorePrefabs(); GameModePreset.Init(); @@ -513,7 +497,6 @@ namespace Barotrauma SubmarineInfo.RefreshSavedSubs(); TitleScreen.LoadState = 75.0f; - yield return CoroutineStatus.Running; GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice); @@ -521,13 +504,11 @@ namespace Barotrauma LightManager = new Lights.LightManager(base.GraphicsDevice); TitleScreen.LoadState = 80.0f; - yield return CoroutineStatus.Running; MainMenuScreen = new MainMenuScreen(this); ServerListScreen = new ServerListScreen(); TitleScreen.LoadState = 85.0f; - yield return CoroutineStatus.Running; #if USE_STEAM if (SteamManager.IsInitialized) @@ -553,12 +534,10 @@ namespace Barotrauma TestScreen = new TestScreen(); TitleScreen.LoadState = 90.0f; - yield return CoroutineStatus.Running; ParticleEditorScreen = new ParticleEditorScreen(); TitleScreen.LoadState = 95.0f; - yield return CoroutineStatus.Running; LevelEditorScreen = new LevelEditorScreen(); SpriteEditorScreen = new SpriteEditorScreen(); @@ -566,8 +545,6 @@ namespace Barotrauma CharacterEditorScreen = new CharacterEditor.CharacterEditorScreen(); CampaignEndScreen = new CampaignEndScreen(); - yield return CoroutineStatus.Running; - #if DEBUG LevelGenerationParams.CheckValidity(); #endif @@ -583,12 +560,7 @@ namespace Barotrauma TitleScreen.LoadState = 100.0f; HasLoaded = true; - if (GameSettings.CurrentConfig.VerboseLogging) - { - DebugConsole.NewMessage("LOADING COROUTINE FINISHED", Color.Lime); - } - yield return CoroutineStatus.Success; - + log("LOADING COROUTINE FINISHED"); } /// @@ -741,11 +713,6 @@ namespace Barotrauma #endif Client?.Update((float)Timing.Step); - - if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) - { - throw new LoadingException(loadingCoroutine.Exception); - } } else if (HasLoaded) { @@ -1174,7 +1141,7 @@ namespace Barotrauma { waitForKeyHit = waitKeyHit; loadingScreenOpen = true; - TitleScreen.LoadState = null; + TitleScreen.LoadState = 0f; return CoroutineManager.StartCoroutine(TitleScreen.DoLoading(loader)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index eddd741f2..b8966494d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -34,6 +34,8 @@ namespace Barotrauma private bool _isCrewMenuOpen = true; private Point crewListEntrySize; + private readonly List traitorButtons = new List(); + /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. /// @@ -95,6 +97,7 @@ namespace Barotrauma { CanBeFocused = false }; + crewArea.RectTransform.NonScaledSize = HUDLayoutSettings.CrewArea.Size; // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) @@ -193,7 +196,7 @@ namespace Barotrauma }; } - var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); + var reports = OrderPrefab.Prefabs.Where(o => o.IsVisibleAsReportButton).OrderBy(o => o.Identifier).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -225,7 +228,8 @@ namespace Barotrauma //report buttons foreach (OrderPrefab orderPrefab in reports) { - if (!orderPrefab.IsReport || orderPrefab.SymbolSprite == null || orderPrefab.Hidden) { continue; } + if (!orderPrefab.IsVisibleAsReportButton) { continue; } + var btn = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: isHorizontal ? ScaleBasis.BothHeight : ScaleBasis.BothWidth), style: null) { OnClicked = (button, userData) => @@ -366,7 +370,7 @@ namespace Barotrauma } } - int iconsVisible = isJobIconVisible ? 5 : 4; + int iconsVisible = isJobIconVisible ? 6 : 5; var nameRelativeWidth = 1.0f // Start padding - paddingRelativeWidth @@ -412,7 +416,6 @@ namespace Barotrauma { AllowMouseWheelScroll = false, CurrentDragMode = GUIListBox.DragMode.DragWithinBox, - HideChildrenOutsideFrame = false, KeepSpaceForScrollBar = false, OnRearranged = OnOrdersRearranged, ScrollBarVisible = false, @@ -438,13 +441,13 @@ namespace Barotrauma Stretch = false }; - var extraIconFrame = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) + var extraIconFrame = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth * 2, 0.8f), layoutGroup.RectTransform), style: null) { CanBeFocused = false, UserData = "extraicons" }; - var soundIconParent = new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform), style: null) + var soundIconParent = new GUIFrame(new RectTransform(new Vector2(0.8f), extraIconFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest), style: null) { CanBeFocused = false, UserData = "soundicons", @@ -469,16 +472,6 @@ namespace Barotrauma Visible = false }; - if (character.IsBot) - { - new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform), style: null) - { - CanBeFocused = false, - UserData = "objectiveicon", - Visible = false - }; - } - new GUIButton(new RectTransform(new Point((int)commandButtonAbsoluteHeight), background.RectTransform), style: "CrewListCommandButton") { ToolTip = TextManager.Get("inputtype.command"), @@ -489,6 +482,44 @@ namespace Barotrauma return true; } }; + if (character.IsBot) + { + new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform, scaleBasis: ScaleBasis.Smallest), style: null) + { + CanBeFocused = false, + UserData = "objectiveicon", + Visible = false + }; + } + else if (GameMain.GameSession is { TraitorsEnabled: true } && GameMain.Client != null && character != Character.Controlled) + { + Client targetClient = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.Character == character); + if (targetClient != null) + { + if (OrderPrefab.Prefabs.TryGet("reporttraitor", out OrderPrefab order)) + { + var voteTraitorBtn = new GUITickBox(new RectTransform(Vector2.One, extraIconFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), label: string.Empty, style: "TraitorVoteButton") + { + UserData = character, + ToolTip = + RichString.Rich( + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{TextManager.Get("traitor.blamebutton")}‖color:end‖\n" + + TextManager.Get("traitor.blamebutton.tooltip")), + OnSelected = (GUITickBox obj) => + { + foreach (var traitorBtn in traitorButtons) + { + //deselect other traitor buttons + if (traitorBtn != obj) { traitorBtn.SetSelected(false, callOnSelected: false); } + } + GameMain.Client?.Vote(VoteType.Traitor, obj.Selected ? targetClient : null); + return true; + } + }; + traitorButtons.Add(voteTraitorBtn); + } + } + } return background; } @@ -498,6 +529,7 @@ namespace Barotrauma if (crewList?.Content.GetChildByUserData(character) is { } component) { crewList.RemoveChild(component); + traitorButtons.RemoveAll(t => t.IsChildOf(component, recursive: true)); } } @@ -1204,7 +1236,7 @@ namespace Barotrauma } } - crewArea.Visible = !(GameMain.GameSession?.GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); + crewArea.Visible = GameMain.GameSession?.GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); guiFrame.AddToGUIUpdateList(); } @@ -1371,7 +1403,8 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) && - commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false)) + commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false) && + Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true }) { if (PlayerInput.IsShiftDown()) { @@ -1556,6 +1589,12 @@ namespace Barotrauma { if (characterComponent.UserData is Character character) { + if (character.Removed) + { + characterComponent.Visible = false; + continue; + } + characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null && (character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f)) @@ -1632,6 +1671,8 @@ namespace Barotrauma } } + traitorButtons.ForEach(btn => btn.Visible = Character.Controlled is { IsDead: false } && btn.UserData as Character != Character.Controlled); + crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), Vector2.Zero, @@ -2395,7 +2436,7 @@ namespace Barotrauma if (!(GetTargetSubmarine() is { } sub)) { return; } shortcutNodes.Clear(); var subItems = sub.GetItems(false); - if (CanFitMoreNodes() && subItems.Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) + if (CanFitMoreNodes() && subItems.Find(i => i.HasTag(Tags.Reactor) && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -2414,7 +2455,7 @@ namespace Barotrauma // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && - subItems.Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) && + subItems.Find(i => i.HasTag(Tags.NavTerminal) && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 89364a512..28556fd9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -157,11 +157,15 @@ namespace Barotrauma SlideshowPlayer?.DrawManually(spriteBatch); - if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) + if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || + CoroutineManager.IsCoroutineRunning("LevelTransition")) { endRoundButton.Visible = false; - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = false; } - return; + if (ReadyCheckButton != null) + { + ReadyCheckButton.Visible = false; + } + return; } if (Submarine.MainSub == null || Level.Loaded == null) { return; } @@ -216,11 +220,15 @@ namespace Barotrauma } break; } - if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) + if (Level.IsLoadedOutpost && + (!ObjectiveManager.AllActiveObjectivesCompleted() && this is not MultiPlayerCampaign)) { allowEndingRound = false; } - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound; } + if (ReadyCheckButton != null) + { + ReadyCheckButton.Visible = allowEndingRound && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0f; + } endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false }; if (endRoundButton.Visible) @@ -384,8 +392,11 @@ namespace Barotrauma protected void TryEndRoundWithFuelCheck(Action onConfirm, Action onReturnToMapScreen) { Submarine.MainSub.CheckFuel(); - SubmarineInfo nextSub = PendingSubmarineSwitch ?? Submarine.MainSub.Info; - bool lowFuel = nextSub.Name == Submarine.MainSub.Info.Name ? Submarine.MainSub.Info.LowFuel : nextSub.LowFuel; + bool lowFuel = Submarine.MainSub.Info.LowFuel; + if (PendingSubmarineSwitch != null) + { + lowFuel = TransferItemsOnSubSwitch ? (lowFuel && PendingSubmarineSwitch.LowFuel) : PendingSubmarineSwitch.LowFuel; + } if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains("reactorfuel")))) { var extraConfirmationBox = @@ -433,11 +444,21 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); } +#if DEBUG + if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) + { + if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } + GUIFrame summaryFrame = GameMain.GameSession.RoundSummary.CreateSummaryFrame(GameMain.GameSession, ""); + GUIMessageBox.MessageBoxes.Add(summaryFrame); + GameMain.GameSession.RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; + } +#endif if (ShowCampaignUI || ForceMapUI) { CampaignUI?.Update(deltaTime); } } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index ab23c52d3..a3be9801c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -284,7 +284,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { yield return CoroutineStatus.Success; } @@ -1014,9 +1014,9 @@ namespace Barotrauma public void LoadState(string filePath) { DebugConsole.Log($"Loading save file for an existing game session ({filePath})"); - SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath, null); + SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath); - string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, "gamesession.xml"); + string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, SaveUtil.GameSessionFileName); XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath); if (doc == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index d2c611e10..fa274ad8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -368,7 +368,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { NextLevel = newLevel; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); @@ -382,7 +382,7 @@ namespace Barotrauma // Event history must be registered before ending the round or it will be cleared GameMain.GameSession.EventManager.RegisterEventHistory(); } - GameMain.GameSession.EndRound("", traitorResults, transitionType); + GameMain.GameSession.EndRound("", transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; if (GUIMessageBox.VisibleBox?.UserData is RoundSummary) @@ -513,17 +513,6 @@ namespace Barotrauma } } -#if DEBUG - if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) - { - if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } - - GUIFrame summaryFrame = GameMain.GameSession.RoundSummary.CreateSummaryFrame(GameMain.GameSession, "", null); - GUIMessageBox.MessageBoxes.Add(summaryFrame); - GameMain.GameSession.RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; - } -#endif - if (ShowCampaignUI || ForceMapUI) { Character.DisableControls = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index d6c8f225d..59031cb75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Tutorials { enum AutoPlayVideo { Yes, No }; - enum TutorialSegmentType { MessageBox, InfoBox, Objective }; + enum SegmentType { MessageBox, InfoBox, Objective }; sealed class Tutorial { @@ -108,7 +108,7 @@ namespace Barotrauma.Tutorials GameMain.GameSession.StartRound(LevelSeed); } - GameMain.GameSession.EventManager.ActiveEvents.Clear(); + GameMain.GameSession.EventManager.ClearEvents(); GameMain.GameSession.EventManager.Enabled = true; GameMain.GameScreen.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 90226c5e0..9c42d6beb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,5 +1,4 @@ -using Barotrauma.Tutorials; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -15,8 +14,6 @@ namespace Barotrauma public static bool IsTabMenuOpen => GameMain.GameSession?.tabMenu != null; public static TabMenu TabMenuInstance => GameMain.GameSession?.tabMenu; - private float prevHudScale; - private TabMenu tabMenu; public bool ToggleTabMenu() @@ -27,7 +24,7 @@ namespace Barotrauma GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu = null; if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } - if (tabMenu == null && !(GameMode is TutorialMode) && !ConversationAction.IsDialogOpen) + if (tabMenu == null && GameMode is not TutorialMode && !ConversationAction.IsDialogOpen) { tabMenu = new TabMenu(); HintManager.OnShowTabMenu(); @@ -49,6 +46,8 @@ namespace Barotrauma private GUITextBlock respawnInfoText; private GUITickBox respawnTickBox; + private GUIImage eventLogNotification; + private void CreateTopLeftButtons() { if (topLeftButtonGroup != null) @@ -96,7 +95,8 @@ namespace Barotrauma OnClicked = (button, userData) => ToggleTabMenu() }; - talentPointNotification = CreateTalentIconNotification(tabMenuButton); + talentPointNotification = CreateNotificationIcon(tabMenuButton); + eventLogNotification = CreateNotificationIcon(tabMenuButton); GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; @@ -121,7 +121,6 @@ namespace Barotrauma return true; } }; - prevHudScale = GameSettings.CurrentConfig.Graphics.HUDScale; } public void AddToGUIUpdateList() @@ -152,7 +151,7 @@ namespace Barotrauma } } - public static GUIImage CreateTalentIconNotification(GUIComponent parent, bool offset = true) + public static GUIImage CreateNotificationIcon(GUIComponent parent, bool offset = true) { GUIImage indicator = new GUIImage(new RectTransform(new Vector2(0.45f), parent.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.BothWidth), style: "TalentPointNotification") { @@ -167,19 +166,22 @@ namespace Barotrauma return indicator; } + public void EnableEventLogNotificationIcon(bool enabled) + { + if (eventLogNotification == null) { return; } + if (!eventLogNotification.Visible && enabled) + { + eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); + } + eventLogNotification.Visible = enabled; + } + public static void UpdateTalentNotificationIndicator(GUIImage indicator) { - if (indicator != null) - { - if (Character.Controlled?.Info == null) - { - indicator.Visible = false; - } - else - { - indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0 && !Character.Controlled.HasUnlockedAllTalents(); - } - } + if (indicator == null) { return; } + indicator.Visible = + Character.Controlled?.Info != null && + Character.Controlled.Info.GetAvailableTalentPoints() > 0 && !Character.Controlled.HasUnlockedAllTalents(); } public void HUDScaleChanged() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 49f2a6fcc..8d222c5a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -1,7 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; -using Barotrauma.Tutorials; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -152,8 +151,8 @@ namespace Barotrauma } // onstartedinteracting.turretperiscope - if (item.HasTag("periscope") && - item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag("turret")) is Turret) + if (item.HasTag(Tags.Periscope) && + item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag(Tags.Turret)) is Turret) { if (DisplayHint($"{hintIdentifierBase}.turretperiscope".ToIdentifier(), variables: new[] @@ -175,6 +174,17 @@ namespace Barotrauma } } + public static void OnStartRepairing(Character character, Repairable repairable) + { + if (repairable.ForceDeteriorationTimer > 0.0f && !character.IsTraitor) + { + CoroutineManager.Invoke(() => + { + DisplayHint($"repairingsabotageditem".ToIdentifier()); + }, delay: 5.0f); + } + } + private static void CheckIsInteracting() { if (!CanDisplayHints()) { return; } @@ -182,7 +192,7 @@ namespace Barotrauma if (Character.Controlled.SelectedItem.GetComponent() is Reactor reactor && reactor.PowerOn && Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable containedItems && - containedItems.Count(i => i.HasTag("reactorfuel")) > 1) + containedItems.Count(i => i.HasTag(Tags.Fuel)) > 1) { if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } } @@ -210,7 +220,7 @@ namespace Barotrauma { if (item.CurrentHull == null) { continue; } if (item.GetComponent() == null) { continue; } - if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } BallastHulls.Add(item.CurrentHull); } } @@ -246,8 +256,14 @@ namespace Barotrauma }); } + if (GameMain.GameSession is { TraitorsEnabled: true }) + { + DisplayHint("traitorsonboard".ToIdentifier()); + DisplayHint("traitorsonboard2".ToIdentifier()); + } yield return CoroutineStatus.Success; } + } public static void OnRoundEnded() @@ -395,8 +411,8 @@ namespace Barotrauma if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; } } - if ((item.HasTag("geneticmaterial") && character.Inventory.FindItemByTag("geneticdevice".ToIdentifier(), recursive: true) != null) || - (item.HasTag("geneticdevice") && character.Inventory.FindItemByTag("geneticmaterial".ToIdentifier(), recursive: true) != null)) + if ((item.HasTag(Tags.GeneticMaterial) && character.Inventory.FindItemByTag(Tags.GeneticMaterial, recursive: true) != null) || + (item.HasTag(Tags.GeneticDevice) && character.Inventory.FindItemByTag(Tags.GeneticDevice, recursive: true) != null)) { if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; } } @@ -437,6 +453,14 @@ namespace Barotrauma }); } + public static void OnRadioJammed(Item radioItem) + { + if (!CanDisplayHints()) { return; } + if (radioItem?.ParentInventory is not CharacterInventory characterInventory) { return; } + if (characterInventory.Owner != Character.Controlled) { return; } + DisplayHint("radiojammed".ToIdentifier()); + } + public static void OnReactorOutOfFuel(Reactor reactor) { if (!CanDisplayHints()) { return; } @@ -450,6 +474,13 @@ namespace Barotrauma }); } + public static void OnAssignedAsTraitor() + { + if (!CanDisplayHints()) { return; } + DisplayHint("assignedastraitor".ToIdentifier()); + DisplayHint("assignedastraitor2".ToIdentifier()); + } + public static void OnAvailableTransition(CampaignMode.TransitionType transitionType) { if (!CanDisplayHints()) { return; } @@ -556,7 +587,7 @@ namespace Barotrauma private static void CheckIfDivingGearOutOfOxygen() { if (!CanDisplayHints()) { return; } - var divingGear = Character.Controlled.GetEquippedItem("diving", InvSlotType.OuterClothes); + var divingGear = Character.Controlled.GetEquippedItem(Tags.DivingGear, InvSlotType.OuterClothes); if (divingGear?.OwnInventory == null) { return; } if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } DisplayHint("ondivinggearoutofoxygen".ToIdentifier(), onUpdate: () => @@ -599,7 +630,7 @@ namespace Barotrauma if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage".ToIdentifier())) { return; } } - static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem("deepdiving", InvSlotType.OuterClothes) is Item; + static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem(Tags.HeavyDivingGear, InvSlotType.OuterClothes) is Item; } static bool IsOnFriendlySub() => Character.Controlled.Submarine is Submarine sub && (sub.TeamID == Character.Controlled.TeamID || sub.TeamID == CharacterTeamType.FriendlyNPC); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs index ad65dcb8f..0a70bc1d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -49,7 +49,7 @@ static class ObjectiveManager public Identifier ParentId { get; set; } - public TutorialSegmentType SegmentType { get; private set; } + public SegmentType SegmentType { get; private set; } public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) { @@ -69,31 +69,31 @@ static class ObjectiveManager private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) { Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value)); AutoPlayVideo = autoPlayVideo; TextContent = textContent; VideoContent = videoContent; - SegmentType = TutorialSegmentType.InfoBox; + SegmentType = SegmentType.InfoBox; } private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) { Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value)); OnClickObjective = onClickObjective; - SegmentType = TutorialSegmentType.MessageBox; + SegmentType = SegmentType.MessageBox; } private Segment(Identifier id, Identifier objectiveTextTag) { Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - SegmentType = TutorialSegmentType.Objective; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value)); + SegmentType = SegmentType.Objective; } public void ConnectMessageBox(Segment messageBoxSegment) { - SegmentType = TutorialSegmentType.MessageBox; + SegmentType = SegmentType.MessageBox; OnClickObjective = messageBoxSegment.OnClickObjective; } } @@ -139,9 +139,9 @@ static class ObjectiveManager VideoPlayer.AddToGUIUpdateList(order: 100); } - public static void TriggerTutorialSegment(Segment segment, bool connectObjective = false) + public static void TriggerSegment(Segment segment, bool connectObjective = false) { - if (segment.SegmentType != TutorialSegmentType.InfoBox) + if (segment.SegmentType != SegmentType.InfoBox) { activeObjectives.Add(segment); AddToObjectiveList(segment, connectObjective); @@ -153,15 +153,15 @@ static class ObjectiveManager ActiveContentSegment = segment; var title = TextManager.Get(segment.Id); - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); - tutorialText = TextManager.ParseInputTypes(tutorialText); + LocalizedString text = TextManager.GetFormatted(segment.TextContent.Tag).Fallback(segment.TextContent.Tag.Value); + text = TextManager.ParseInputTypes(text); switch (segment.AutoPlayVideo) { case AutoPlayVideo.Yes: infoBox = CreateInfoFrame( title, - tutorialText, + text, segment.TextContent.Width, segment.TextContent.Height, segment.TextContent.Anchor, @@ -171,7 +171,7 @@ static class ObjectiveManager case AutoPlayVideo.No: infoBox = CreateInfoFrame( title, - tutorialText, + text, segment.TextContent.Width, segment.TextContent.Height, segment.TextContent.Anchor, @@ -182,31 +182,54 @@ static class ObjectiveManager } } - public static void CompleteTutorialSegment(Identifier segmentId) + public static void CompleteSegment(Identifier segmentId) { if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted) { return; } - if (!MarkSegmentCompleted(segment)) + CompleteSegment(segment, failed: false); + } + + public static void FailSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment) { return; } + CompleteSegment(segment, failed: true); + } + + private static void CompleteSegment(Segment segment, bool failed = false) + { + if (failed) + { + if (!MarkSegmentFailed(segment)) { return; } + } + else + { + if (!MarkSegmentCompleted(segment)) { return; } + } if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) { - GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segmentId}:Completed"); - } - else if (GameMain.GameSession?.GameMode is CampaignMode campaign) - { - GameAnalyticsManager.AddDesignEvent($"Tutorial:CampaignMode:{segmentId}:Completed"); - campaign?.CampaignMetadata?.SetValue(segmentId, true); + GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segment.Id}:{(failed ? "Failed" : "Completed")}"); } } - public static bool MarkSegmentCompleted(Segment segment, bool flash = true) + private static bool MarkSegmentCompleted(Segment segment, bool flash = true) + { + return MarkSegment(segment, "ObjectiveIndicatorCompleted", flash, flashColor: GUIStyle.Green); + } + + private static bool MarkSegmentFailed(Segment segment, bool flash = true) + { + return MarkSegment(segment, "MissionFailedIcon", flash, flashColor: GUIStyle.Red); + } + + private static bool MarkSegment(Segment segment, string iconStyleName, bool flash, Color flashColor) { segment.IsCompleted = true; - if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) + if (GUIStyle.GetComponentStyle(iconStyleName) is GUIComponentStyle style) { if (segment.ObjectiveStateIndicator.Style == style) { @@ -216,21 +239,17 @@ static class ObjectiveManager } if (flash) { - segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); - } + segment.ObjectiveStateIndicator.Parent.Flash(color: flashColor, flashDuration: 0.35f, useRectangleFlash: true); + } segment.ObjectiveButton.OnClicked = null; segment.ObjectiveButton.CanBeFocused = false; return true; } - public static void RemoveTutorialSegment(Identifier segmentId) + public static void RemoveSegment(Identifier segmentId) { if (GetActiveObjective(segmentId) is not Segment segment) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) - { - DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{tutorialMode.Tutorial?.Identifier}\" but it isn't active!"); - } return; } segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); @@ -400,10 +419,10 @@ static class ObjectiveManager void SetButtonBehavior(Segment segment) { - segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; + segment.ObjectiveButton.CanBeFocused = segment.SegmentType != SegmentType.Objective; segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => { - if (segment.SegmentType == TutorialSegmentType.InfoBox) + if (segment.SegmentType == SegmentType.InfoBox) { if (segment.AutoPlayVideo == AutoPlayVideo.Yes) { @@ -414,7 +433,7 @@ static class ObjectiveManager ShowSegmentText(segment); } } - else if (segment.SegmentType == TutorialSegmentType.MessageBox) + else if (segment.SegmentType == SegmentType.MessageBox) { segment.OnClickObjective?.Invoke(); } @@ -438,8 +457,8 @@ static class ObjectiveManager ContentRunning = true; ActiveContentSegment = segment; infoBox = CreateInfoFrame( - TextManager.Get(segment.Id), - TextManager.Get(segment.TextContent.Tag), + TextManager.Get(segment.Id).Fallback(segment.Id.Value), + TextManager.Get(segment.TextContent.Tag).Fallback(segment.TextContent.Tag.Value), segment.TextContent.Width, segment.TextContent.Height, segment.TextContent.Anchor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 8ba436270..f2d7b680f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -10,12 +10,12 @@ namespace Barotrauma { internal partial class ReadyCheck { - private static LocalizedString readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); + private static LocalizedString ReadyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); - private static LocalizedString readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", + private static LocalizedString ReadyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", ("[ready]", ready.ToString()), ("[total]", total.ToString())); - private static LocalizedString readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); + private static LocalizedString ReadyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); private static readonly LocalizedString readyCheckHeader = TextManager.Get("ReadyCheck.Title"); @@ -42,7 +42,7 @@ namespace Barotrauma { Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); - msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; + msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange) { UserData = TimerData }; @@ -82,9 +82,15 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), resultsBox.Content.RectTransform)) { UserData = UserListData }; - foreach (var (id, _) in Clients) + foreach (var (id, status) in Clients) { Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.SessionId == id); + if (client == null) + { + string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n"); + DebugConsole.AddWarning($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); + continue; + } GUIFrame container = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = id }; GUILayoutGroup frame = new GUILayoutGroup(new RectTransform(Vector2.One, container.RectTransform), isHorizontal: true) { Stretch = true }; @@ -92,11 +98,6 @@ namespace Barotrauma JobPrefab? jobPrefab = client?.Character?.Info?.Job?.Prefab; - if (client == null) - { - string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n"); - DebugConsole.ThrowError($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); - } if (jobPrefab?.Icon != null) { @@ -105,7 +106,8 @@ namespace Barotrauma } new GUITextBlock(new RectTransform(new Vector2(0.75f, 1), frame.RectTransform), client?.Name ?? $"Unknown ID {id}", jobPrefab?.UIColor ?? Color.White, textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; - new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData }; + var statusIcon = new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData }; + UpdateStatusIcon(statusIcon, status); } resultsBox.Buttons[0].OnClicked = delegate @@ -214,10 +216,7 @@ namespace Barotrauma case ReadyCheckState.Update: ReadyStatus newState = (ReadyStatus)inc.ReadByte(); byte targetId = inc.ReadByte(); - if (crewManager.ActiveReadyCheck != null) - { - crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); - } + crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); break; case ReadyCheckState.End: ushort count = inc.ReadUInt16(); @@ -242,7 +241,7 @@ namespace Barotrauma int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; - GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); + GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, ReadyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } private void UpdateState(byte id, ReadyStatus status) @@ -253,31 +252,28 @@ namespace Barotrauma } if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; } - if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; } - // for some reason FindChild doesn't work here? - foreach (GUIComponent child in userList.Content.Children) + var child = userList.Content.FindChild(id); + if (child?.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { return; } + UpdateStatusIcon(image, status); + } + + private static void UpdateStatusIcon(GUIImage image, ReadyStatus status) + { + string style; + switch (status) { - if (child.UserData is not byte b || b != id) { continue; } - - if (child.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { continue; } - - string style; - switch (status) - { - case ReadyStatus.Yes: - style = "MissionCompletedIcon"; - break; - case ReadyStatus.No: - style = "MissionFailedIcon"; - break; - default: - return; - } - - image.ApplyStyle(GUIStyle.GetComponentStyle(style)); + case ReadyStatus.Yes: + style = "MissionCompletedIcon"; + break; + case ReadyStatus.No: + style = "MissionFailedIcon"; + break; + default: + return; } + image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } private static void SendState(ReadyStatus status) @@ -303,7 +299,7 @@ namespace Barotrauma return; } - GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); + GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index f6730cbd5..96fb2522a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -10,6 +10,9 @@ namespace Barotrauma { class RoundSummary { + private float crewListAnimDelay = 0.25f; + private float missionIconAnimDelay; + private const float jobColumnWidthPercentage = 0.11f; private const float characterColumnWidthPercentage = 0.44f; private const float statusColumnWidthPercentage = 0.45f; @@ -44,7 +47,7 @@ namespace Barotrauma } } - public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, List traitorResults, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { bool singleplayer = GameMain.NetworkMember == null; bool gameOver = @@ -70,7 +73,7 @@ namespace Barotrauma //crew panel ------------------------------------------------------------------------------- - GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.4f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); GUIFrame crewFrameInner = new GUIFrame(new RectTransform(new Point(crewFrame.Rect.Width - padding * 2, crewFrame.Rect.Height - padding * 2), crewFrame.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner.RectTransform, Anchor.Center)) @@ -82,7 +85,14 @@ namespace Barotrauma TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); - CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2)); + var crewList = CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2), traitorResults); + if (traitorResults != null && traitorResults.Value.VotedAsTraitorClientSessionId > 0) + { + var traitorInfoPanel = CreateTraitorInfoPanel(crewList.Content, traitorResults.Value, crewListAnimDelay); + traitorInfoPanel.RectTransform.SetAsFirstChild(); + var spacing = new GUIFrame(new RectTransform(new Point(0, GUI.IntScale(20)), crewList.Content.RectTransform), style: null); + spacing.RectTransform.RepositionChildInHierarchy(1); + } //another crew frame for the 2nd team in combat missions if (gameSession.Missions.Any(m => m is CombatMission)) @@ -98,7 +108,7 @@ namespace Barotrauma var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); - CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2)); + CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2), traitorResults); } //header ------------------------------------------------------------------------------- @@ -111,71 +121,6 @@ namespace Barotrauma headerText, textAlignment: Alignment.BottomLeft, font: GUIStyle.LargeFont, wrap: true); } - //traitor panel ------------------------------------------------------------------------------- - - if (traitorResults != null && traitorResults.Any()) - { - GUIFrame traitorframe = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: crewFrame.RectTransform.MinSize)); - rightPanels.Add(traitorframe); - GUIFrame traitorframeInner = new GUIFrame(new RectTransform(new Point(traitorframe.Rect.Width - padding * 2, traitorframe.Rect.Height - padding * 2), traitorframe.RectTransform, Anchor.Center), style: "InnerFrame"); - - var traitorContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), traitorframeInner.RectTransform, Anchor.Center)) - { - Stretch = true - }; - - var traitorHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorContent.RectTransform), - TextManager.Get("traitors"), font: GUIStyle.SubHeadingFont); - traitorHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(traitorHeader.Rect.Height * 2.0f)); - - GUIListBox listBox = CreateCrewList(traitorContent, traitorResults.SelectMany(tr => tr.Characters.Select(c => c.Info))); - - foreach (var traitorResult in traitorResults) - { - var traitorMission = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == traitorResult.MissionIdentifier); - if (traitorMission == null) { continue; } - - //spacing - new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(25)), listBox.Content.RectTransform), style: null); - - var traitorResultHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), listBox.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - new GUIImage(new RectTransform(new Point(traitorResultHorizontal.Rect.Height), traitorResultHorizontal.RectTransform), traitorMission.Icon, scaleToFit: true) - { - Color = traitorMission.IconColor - }; - - LocalizedString traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); - if (!traitorMessage.IsNullOrEmpty()) - { - var textContent = new GUILayoutGroup(new RectTransform(Vector2.One, traitorResultHorizontal.RectTransform)) - { - RelativeSpacing = 0.025f - }; - - var traitorStatusText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - TextManager.Get(traitorResult.Success ? "missioncompleted" : "missionfailed"), - textColor: traitorResult.Success ? GUIStyle.Green : GUIStyle.Red, font: GUIStyle.SubHeadingFont); - - var traitorMissionInfo = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - traitorMessage, font: GUIStyle.SmallFont, wrap: true); - - traitorResultHorizontal.Recalculate(); - - traitorStatusText.CalculateHeightFromText(); - traitorMissionInfo.CalculateHeightFromText(); - traitorStatusText.RectTransform.MinSize = new Point(0, traitorStatusText.Rect.Height); - traitorMissionInfo.RectTransform.MinSize = new Point(0, traitorMissionInfo.Rect.Height); - textContent.RectTransform.MaxSize = new Point(int.MaxValue, (int)((traitorStatusText.Rect.Height + traitorMissionInfo.Rect.Height) * 1.2f)); - traitorResultHorizontal.RectTransform.MinSize = new Point(0, traitorStatusText.RectTransform.MinSize.Y + traitorMissionInfo.RectTransform.MinSize.Y); - } - } - } - //reputation panel ------------------------------------------------------------------------------- var campaignMode = gameMode as CampaignMode; @@ -185,7 +130,7 @@ namespace Barotrauma rightPanels.Add(reputationframe); GUIFrame reputationframeInner = new GUIFrame(new RectTransform(new Point(reputationframe.Rect.Width - padding * 2, reputationframe.Rect.Height - padding * 2), reputationframe.RectTransform, Anchor.Center), style: "InnerFrame"); - var reputationContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), reputationframeInner.RectTransform, Anchor.Center)) + var reputationContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), reputationframeInner.RectTransform, Anchor.Center)) { Stretch = true }; @@ -199,15 +144,15 @@ namespace Barotrauma //mission panel ------------------------------------------------------------------------------- - GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.3f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); + GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.4f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); GUILayoutGroup missionFrameContent = new GUILayoutGroup(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center)) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.03f }; GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), missionFrameContent.RectTransform, Anchor.Center), style: "InnerFrame"); - var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) + var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) { Stretch = true }; @@ -227,16 +172,9 @@ namespace Barotrauma } } - if (missionsToDisplay.Any()) - { - var missionHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), - TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); - missionHeader.RectTransform.MinSize = new Point(0, (int)(missionHeader.Rect.Height * 1.2f)); - } - GUIListBox missionList = new GUIListBox(new RectTransform(Vector2.One, missionContent.RectTransform, Anchor.Center)) { - Padding = new Vector4(4, 10, 0, 0) * GUI.Scale + Spacing = GUI.IntScale(15) }; missionList.ContentBackground.Color = Color.Transparent; @@ -255,120 +193,76 @@ namespace Barotrauma { CanBeFocused = false }; - endText.RectTransform.MinSize = new Point(0, endText.Rect.Height); - var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionList.Content.RectTransform), style: "HorizontalLine"); - line.RectTransform.NonScaledSize = new Point(line.Rect.Width, GUI.IntScale(5.0f)); } - foreach (Mission displayedMission in missionsToDisplay) + float animDelay = missionIconAnimDelay; + foreach (Mission mission in missionsToDisplay) { - var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), missionList.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) - { - RelativeSpacing = 0.025f, - Stretch = true - }; + var textContent = new List(); - LocalizedString missionMessage = - selectedMissions.Contains(displayedMission) ? - displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : - displayedMission.Description; - GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height)), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) + if (selectedMissions.Contains(mission)) { - Color = displayedMission.Prefab.IconColor, - HoverColor = displayedMission.Prefab.IconColor, - SelectedColor = displayedMission.Prefab.IconColor - }; - missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.9f)); - if (selectedMissions.Contains(displayedMission)) - { - new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); - } + textContent.Add(mission.Completed ? mission.SuccessMessage : mission.FailureMessage); - var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), missionContentHorizontal.RectTransform)) - { - AbsoluteSpacing = GUI.IntScale(5) - }; - missionContentHorizontal.Recalculate(); - var missionNameTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - displayedMission.Name, font: GUIStyle.SubHeadingFont); - if (displayedMission.Difficulty.HasValue) - { - var groupSize = missionNameTextBlock.Rect.Size; - groupSize.X -= (int)(missionNameTextBlock.Padding.X + missionNameTextBlock.Padding.Z); - var indicatorGroup = new GUILayoutGroup(new RectTransform(groupSize, missionTextContent.RectTransform) { AbsoluteOffset = new Point((int)missionNameTextBlock.Padding.X, 0) }, - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - AbsoluteSpacing = 1 - }; - var difficultyColor = displayedMission.GetDifficultyColor(); - for (int i = 0; i < displayedMission.Difficulty; i++) - { - new GUIImage(new RectTransform(Vector2.One, indicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) - { - CanBeFocused = false, - Color = difficultyColor - }; - } - } + var repText = mission.GetReputationRewardText(); + if (!repText.IsNullOrEmpty()) { textContent.Add(repText); } - var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - RichString.Rich(missionMessage), wrap: true); - if (selectedMissions.Contains(displayedMission)) - { - RichString reputationText = displayedMission.GetReputationRewardText(); - if (!reputationText.IsNullOrEmpty()) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true); - } - - int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); + int totalReward = mission.GetFinalReward(Submarine.MainSub); if (totalReward > 0) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && displayedMission.Completed) + textContent.Add(mission.GetMissionRewardText(Submarine.MainSub)); + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && mission.Completed) { var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); - RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + RichString yourShareString = TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}")); + textContent.Add(yourShareString); } } } } - - if (displayedMission != missionsToDisplay.Last()) + else { - var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), missionList.Content.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(15)) }, style: null); - new GUIFrame(new RectTransform(new Vector2(0.8f, 1.0f), spacing.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.1f, 0.0f) }, "HorizontalLine"); + var repText = mission.GetReputationRewardText(); + if (!repText.IsNullOrEmpty()) { textContent.Add(repText); } + textContent.Add(mission.GetMissionRewardText(Submarine.MainSub)); + textContent.Add(mission.Description); + textContent.AddRange(mission.ShownMessages); } - foreach (GUIComponent child in missionTextContent.Children) + CreateMissionEntry( + missionList.Content, + mission.Name, + textContent, + mission.Difficulty ?? 0, + mission.Prefab.Icon, mission.Prefab.IconColor, + out GUIImage missionIcon); + + if (selectedMissions.Contains(mission)) { - child.RectTransform.IsFixedSize = true; + UpdateMissionStateIcon(mission.Completed, missionIcon, animDelay); + animDelay += 0.25f; } - missionTextContent.RectTransform.MinSize = new Point(0, missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing)); - missionContentHorizontal.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Rect.Height / missionTextContent.RectTransform.RelativeSize.Y)); } if (!missionsToDisplay.Any()) { - var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) { RelativeSpacing = 0.025f, - Stretch = true + Stretch = true, + CanBeFocused = true }; - GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); - missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)); + GUIImage missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), TextManager.Get("nomission"), font: GUIStyle.LargeFont); } - /*missionContentHorizontal.Recalculate(); - missionContent.Recalculate(); - missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); - missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width);*/ + gameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content, traitorResults); + + AddSeparators(missionList.Content); ContinueButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), ButtonArea.RectTransform), TextManager.Get("Close")); ButtonArea.RectTransform.NonScaledSize = new Point(ButtonArea.Rect.Width, ContinueButton.Rect.Height); @@ -405,10 +299,7 @@ namespace Barotrauma public void CreateReputationInfoPanel(GUIComponent parent, CampaignMode campaignMode) { - GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) - { - Padding = new Vector4(4, 10, 0, 0) * GUI.Scale - }; + GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)); reputationList.ContentBackground.Color = Color.Transparent; foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) @@ -506,6 +397,171 @@ namespace Barotrauma } } + private static GUIComponent CreateTraitorInfoPanel(GUIComponent parent, TraitorManager.TraitorResults traitorResults, float iconAnimDelay) + { + var traitorCharacter = traitorResults.GetTraitorClient()?.Character; + + string resultTag = + traitorResults.VotedCorrectTraitor ? + traitorResults.ObjectiveSuccessful ? "traitor.blameresult.correct.objectivesuccessful" : "traitor.blameresult.correct.objectivefailed" : + "traitor.blameresult.failure"; + + var textContent = new List() + { + TextManager.GetWithVariable("traitor.blameresult", "[name]", traitorCharacter?.Name ?? "unknown"), + TextManager.Get(resultTag) + }; + + if (traitorResults.MoneyPenalty > 0) + { + textContent.Add( + TextManager.GetWithVariable( + "traitor.blameresult.failure.penalty", + "[money]", + TextManager.FormatCurrency(traitorResults.MoneyPenalty, includeCurrencySymbol: false))); + } + + var icon = GUIStyle.GetComponentStyle("TraitorMissionIcon")?.GetDefaultSprite(); + + var content = CreateMissionEntry( + parent, + string.Empty, + textContent, + difficultyIconCount: 0, + icon, GUIStyle.Red, + out GUIImage missionIcon); + UpdateMissionStateIcon(traitorResults.VotedCorrectTraitor, missionIcon, iconAnimDelay); + return content; + } + + public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedString header, List textContent, int difficultyIconCount, + Sprite icon, Color iconColor, out GUIImage missionIcon) + { + int spacing = GUI.IntScale(5); + + int defaultLineHeight = (int)GUIStyle.Font.MeasureChar('T').Y; + + //make the icon big enough for header + some lines of text + int iconSize = (int)(GUIStyle.SubHeadingFont.MeasureChar('T').Y + defaultLineHeight * 6); + + GUILayoutGroup content = new GUILayoutGroup(new RectTransform(new Point(parent.Rect.Width, iconSize), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + AbsoluteSpacing = spacing, + CanBeFocused = true + }; + if (icon != null) + { + missionIcon = new GUIImage(new RectTransform(new Point(iconSize), content.RectTransform), icon, null, true) + { + Color = iconColor, + HoverColor = iconColor, + SelectedColor = iconColor, + CanBeFocused = false + }; + missionIcon.RectTransform.IsFixedSize = true; + } + else + { + missionIcon = null; + } + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), content.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.TopLeft) + { + AbsoluteSpacing = spacing + }; + content.Recalculate(); + + RichString missionNameString = RichString.Rich(header); + List contentStrings = new List(textContent.Select(t => RichString.Rich(t))); + + if (!header.IsNullOrEmpty()) + { + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), + missionNameString, font: GUIStyle.SubHeadingFont, wrap: true); + nameText.RectTransform.MinSize = new Point(0, (int)nameText.TextSize.Y); + } + + GUILayoutGroup difficultyIndicatorGroup = null; + if (difficultyIconCount > 0) + { + difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, defaultLineHeight), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + AbsoluteSpacing = 1 + }; + difficultyIndicatorGroup.RectTransform.MinSize = new Point(0, defaultLineHeight); + var difficultyColor = Mission.GetDifficultyColor(difficultyIconCount); + for (int i = 0; i < difficultyIconCount; i++) + { + new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) + { + CanBeFocused = false, + Color = difficultyColor + }; + } + } + + GUITextBlock firstContentText = null; + foreach (var contentString in contentStrings) + { + var text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), contentString, wrap: true); + text.RectTransform.MinSize = new Point(0, (int)text.TextSize.Y); + firstContentText ??= text; + } + if (difficultyIndicatorGroup != null && firstContentText != null) + { + //make the icons align with the text content + difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)firstContentText.Padding.X, 0); + } + missionTextGroup.RectTransform.MinSize = + new Point(0, missionTextGroup.Children.Sum(c => c.Rect.Height + missionTextGroup.AbsoluteSpacing) - missionTextGroup.AbsoluteSpacing); + missionTextGroup.Recalculate(); + content.RectTransform.MinSize = new Point(0, Math.Max(missionTextGroup.Rect.Height, iconSize)); + + return content; + } + + public static void AddSeparators(GUIComponent container) + { + var children = container.Children.ToList(); + if (children.Count < 2) { return; } + + var lastChild = children.Last(); + foreach (var child in children) + { + if (child != lastChild) + { + var separator = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), container.RectTransform), style: "HorizontalLine"); + separator.RectTransform.RepositionChildInHierarchy(container.GetChildIndex(child) + 1); + } + } + } + + public static void UpdateMissionStateIcon(bool success, GUIImage missionIcon, float delay = 0.5f) + { + if (missionIcon == null) { return; } + string style = success ? "MissionCompletedIcon" : "MissionFailedIcon"; + GUIImage stateIcon = missionIcon.GetChild(); + if (string.IsNullOrEmpty(style)) + { + if (stateIcon != null) + { + stateIcon.Visible = false; + } + } + else + { + bool wasVisible = stateIcon is { Visible: true }; + stateIcon ??= new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform, Anchor.Center), style, scaleToFit: true); + stateIcon.Visible = true; + if (!wasVisible) + { + stateIcon.FadeIn(delay, 0.15f); + stateIcon.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.0f + delay); + } + } + } + + private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; @@ -568,7 +624,7 @@ namespace Barotrauma return TextManager.GetWithVariables(textTag, ("[sub]", subName), ("[location]", locationName)); } - private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos) + private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos, TraitorManager.TraitorResults? traitorResults) { var headerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform, Anchor.TopCenter, minSize: new Point(0, (int)(30 * GUI.Scale))) { }, isHorizontal: true) { @@ -602,16 +658,19 @@ namespace Barotrauma headerFrame.RectTransform.RelativeSize -= new Vector2(crewList.ScrollBar.RectTransform.RelativeSize.X, 0.0f); + float delay = crewListAnimDelay; foreach (CharacterInfo characterInfo in characterInfos) { if (characterInfo == null) { continue; } - CreateCharacterElement(characterInfo, crewList); + CreateCharacterElement(characterInfo, crewList, traitorResults, delay); + delay += crewListAnimDelay; } + missionIconAnimDelay = delay; return crewList; } - private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox) + private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox, TraitorManager.TraitorResults? traitorResults, float animDelay) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(45)), listBox.Content.RectTransform), style: "ListBoxElement") { @@ -684,9 +743,31 @@ namespace Barotrauma GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(statusText.Value, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); + + frame.FadeIn(animDelay, 0.15f); + foreach (var child in frame.GetAllChildren()) + { + child.FadeIn(animDelay, 0.15f); + } + + if (traitorResults.HasValue && GameMain.NetworkMember != null) + { + var clientVotedAsTraitor = traitorResults.Value.GetTraitorClient(); + bool isTraitor = clientVotedAsTraitor != null && clientVotedAsTraitor.Character == character; + if (isTraitor) + { + var img = new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.CenterRight), style: "TraitorVoteButton") + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.GetWithVariable("traitor.blameresult", "[name]", characterInfo.Name) + }; + img.FadeIn(1.0f + animDelay, 0.15f); + img.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.5f + animDelay); + } + } } - private GUIFrame CreateReputationElement(GUIComponent parent, + private static GUIFrame CreateReputationElement(GUIComponent parent, LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 289b08e4b..54b27c3e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -42,16 +43,29 @@ namespace Barotrauma if (limbSlotIcons == null) { limbSlotIcons = new Dictionary(); - int margin = 2; - limbSlotIcons.Add(InvSlotType.Headset, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.InnerClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.Card, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(640 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + foreach (InvSlotType invSlotType in Enum.GetValues(typeof(InvSlotType))) + { + var sprite = GUIStyle.GetComponentStyle($"InventorySlot.{invSlotType}")?.GetDefaultSprite(); + if (sprite != null) + { + limbSlotIcons.Add(invSlotType, sprite); + } + } - limbSlotIcons.Add(InvSlotType.Head, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(896 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.LeftHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(634, 0, 128, 128))); - limbSlotIcons.Add(InvSlotType.RightHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(762, 0, 128, 128))); - limbSlotIcons.Add(InvSlotType.OuterClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.Bag, new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(639, 926, 128,80))); + int margin = 2; + AddIfMissing(InvSlotType.Headset, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.InnerClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.Card, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(640 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.Head, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(896 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.LeftHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(634, 0, 128, 128))); + AddIfMissing(InvSlotType.RightHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(762, 0, 128, 128))); + AddIfMissing(InvSlotType.OuterClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.Bag, new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(639, 926, 128,80))); + + static void AddIfMissing(InvSlotType slotType, Sprite sprite) + { + limbSlotIcons.TryAdd(slotType, sprite); + } } return limbSlotIcons; } @@ -179,11 +193,30 @@ namespace Barotrauma BackgroundFrame = frame; } - protected override bool HideSlot(int i) + public override bool HideSlot(int i) { if (visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty())) { return true; } - if (CharacterHealth.OpenHealthWindow != Character.Controlled?.CharacterHealth && SlotTypes[i] == InvSlotType.HealthInterface) { return true; } + if (SlotTypes[i] == InvSlotType.HealthInterface) + { + //hide health interface slot unless this character's health window is open + if (CharacterHealth.OpenHealthWindow == null || Character.Controlled == null) + { + return true; + } + if (character == Character.Controlled) + { + bool ownHealthWindowOpen = CharacterHealth.OpenHealthWindow == Character.Controlled.CharacterHealth; + if (!ownHealthWindowOpen) { return true; } + } + else if (character == Character.Controlled.SelectedCharacter) + { + bool otherCharacterHealthWindowOpen = CharacterHealth.OpenHealthWindow == Character.Controlled?.SelectedCharacter?.CharacterHealth; + if (!otherCharacterHealthWindowOpen) { return true; } + //can't access the health interface slot of a non-incapacitated player (bots are fine though) + if (character.IsPlayer && !character.IsIncapacitated) { return true; } + } + } if (layout == Layout.Default) { @@ -216,6 +249,11 @@ namespace Barotrauma return false; } + public void RefreshSlotPositions() + { + SetSlotPositions(CurrentLayout); + } + private void SetSlotPositions(Layout layout) { int spacing = GUI.IntScale(5); @@ -450,6 +488,9 @@ namespace Barotrauma ((s.SlotIndex < 0 || s.SlotIndex >= slots.Length || slots[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); //remove highlighted subinventory slots that refer to items no longer in this inventory highlightedSubInventorySlots.RemoveWhere(s => s.Item != null && s.ParentInventory == this && s.Item.ParentInventory != this); + //remove highlighted subinventory slots if we're dragging that item out of the inventory + highlightedSubInventorySlots.RemoveWhere(s => s.Item != null && s.ParentInventory == this && DraggingItems.Contains(s.Item)); + tempHighlightedSubInventorySlots.Clear(); tempHighlightedSubInventorySlots.AddRange(highlightedSubInventorySlots); foreach (var highlightedSubInventorySlot in tempHighlightedSubInventorySlots) @@ -525,6 +566,7 @@ namespace Barotrauma var itemContainer = item.GetComponent(); if (itemContainer != null && itemContainer.KeepOpenWhenEquippedBy(character) && + !DraggingItems.Contains(item) && character.CanAccessInventory(itemContainer.Inventory) && !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) { @@ -538,9 +580,11 @@ namespace Barotrauma if (doubleClickedItems.Any()) { var quickUseAction = GetQuickUseAction(doubleClickedItems.First(), true, true, true); + int itemCount = 0; foreach (Item doubleClickedItem in doubleClickedItems) { QuickUseItem(doubleClickedItem, true, true, true, quickUseAction, playSound: doubleClickedItem == doubleClickedItems.First()); + itemCount++; //only use one item if we're equipping or using it as a treatment if (quickUseAction == QuickUseAction.Equip || quickUseAction == QuickUseAction.UseTreatment) { @@ -556,6 +600,12 @@ namespace Barotrauma { break; } + if ((quickUseAction == QuickUseAction.TakeFromContainer || quickUseAction == QuickUseAction.PutToEquippedItem) && + doubleClickedItem.ParentInventory != null && + itemCount >= doubleClickedItem.Prefab.GetMaxStackSize(doubleClickedItem.ParentInventory)) + { + break; + } } } @@ -789,10 +839,30 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.HeldItems.Any(i => + else if (character.HeldItems.FirstOrDefault(i => i.OwnInventory != null && - (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer) { + if (allowEquip) + { + if (!character.HasEquippedItem(item)) + { + if (equippedContainer.GetComponent() is { QuickUseMovesItemsInside: false}) + { + //put the item in a hand slot if that hand is free + if ((item.AllowedSlots.Contains(InvSlotType.RightHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) == null) || + (item.AllowedSlots.Contains(InvSlotType.LeftHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) == null)) + { + return QuickUseAction.Equip; + } + } + } + //equipped -> attempt to unequip + else if (item.AllowedSlots.Contains(InvSlotType.Any)) + { + return QuickUseAction.Unequip; + } + } return QuickUseAction.PutToEquippedItem; } else if (allowEquip) //doubleclicked and no other inventory is selected @@ -1171,5 +1241,11 @@ namespace Barotrauma GUIComponent.DrawToolTip(spriteBatch, highlightedQuickUseSlot.QuickUseButtonToolTip, highlightedQuickUseSlot.EquipButtonRect); } } + + public void ClientEventWrite(IWriteMessage msg, Character.InventoryStateEventData extraData) + { + SharedWrite(msg, extraData.SlotRange); + syncItemsDelay = 1.0f; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 0be3b921f..f7062ac72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -185,9 +185,9 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - Color color = item.GetSpriteColor(withHighlight: true); + Color color = overrideColor ?? item.GetSpriteColor(withHighlight: true); if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured @@ -202,7 +202,7 @@ namespace Barotrauma.Items.Components weldSpritePos.Y = -weldSpritePos.Y; weldedSprite.Draw(spriteBatch, - weldSpritePos, item.SpriteColor * (stuck / 100.0f), scale: item.Scale); + weldSpritePos, overrideColor ?? (item.SpriteColor * (stuck / 100.0f)), scale: item.Scale); } if (openState >= 1.0f) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs index a7229fbf9..761d5b394 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { public Vector2 DrawSize => Vector2.Zero; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (!editing) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 5380f8940..d0f607e20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components get { return item.Rect.Size.ToVector2(); } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { @@ -50,7 +50,17 @@ namespace Barotrauma.Items.Components Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.4f); - item.Sprite.Draw( + Sprite sprite = item.Sprite; + foreach (ContainedItemSprite containedSprite in item.Prefab.ContainedSprites) + { + if (containedSprite.UseWhenAttached) + { + sprite = containedSprite.Sprite; + break; + } + } + + sprite.Draw( spriteBatch, new Vector2(attachPos.X, -attachPos.Y), item.SpriteColor * 0.5f, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index e19b1dd82..e1b21fe1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -1,11 +1,8 @@ -using System; +using Barotrauma.IO; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.IO; namespace Barotrauma.Items.Components { @@ -48,33 +45,36 @@ namespace Barotrauma.Items.Components return; } - foreach (ContentXElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) + if (characterInfo.Ragdoll.MainElement?.Elements() is { } limbElements) { - if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - - ContentXElement spriteElement = limbElement.GetChildElement("sprite"); - if (spriteElement == null) { continue; } - - ContentPath contentPath = spriteElement.GetAttributeContentPath("texture"); - - string spritePath = characterInfo.ReplaceVars(contentPath.Value); - string fileName = Path.GetFileNameWithoutExtension(spritePath); - - //go through the files in the directory to find a matching sprite - foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) + foreach (ContentXElement limbElement in limbElements) { - if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } + + ContentXElement spriteElement = limbElement.GetChildElement("sprite"); + if (spriteElement == null) { continue; } + + ContentPath contentPath = spriteElement.GetAttributeContentPath("texture"); + + string spritePath = characterInfo.ReplaceVars(contentPath.Value); + string fileName = Path.GetFileNameWithoutExtension(spritePath); + + //go through the files in the directory to find a matching sprite + foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { - continue; + if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string fileWithoutTags = Path.GetFileNameWithoutExtension(file); + fileWithoutTags = fileWithoutTags.Split('[', ']').First(); + if (fileWithoutTags != fileName) { continue; } + Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; } - string fileWithoutTags = Path.GetFileNameWithoutExtension(file); - fileWithoutTags = fileWithoutTags.Split('[', ']').First(); - if (fileWithoutTags != fileName) { continue; } - Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; } - - break; } if (characterInfo.Wearables != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index cd0b92161..099f54b34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { currentCrossHairScale = currentCrossHairPointerScale = cam == null ? 1.0f : cam.Zoom; if (crosshairSprite != null) @@ -118,6 +118,7 @@ namespace Barotrauma.Items.Components else if (chargeSoundChannel != null) { chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index 93769ab4b..35f3edba3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Items.Components private int spraySetting = 0; private readonly Point[] sprayArray = new Point[8]; - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (character == null || !character.IsKeyDown(InputType.Aim)) return; @@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components if (body.UserData is Item item) { var door = item.GetComponent(); - if (door != null && door.CanBeTraversed) { continue; } + if (door != null && (door.IsOpen || door.IsBroken)) { continue; } } targetHull = null; @@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { #if DEBUG if (GameMain.DebugDraw && Character.Controlled != null && Character.Controlled.IsKeyDown(InputType.Aim)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index ebd078a7e..32dca442a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -150,7 +150,9 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } - public bool LockGuiFramePosition; + private GUIDragHandle guiFrameDragHandle; + + private bool guiFrameUpdatePending; [Serialize(false, IsPropertySaveable.No)] public bool AllowUIOverlap @@ -466,7 +468,21 @@ namespace Barotrauma.Items.Components GuiFrame?.AddToGUIUpdateList(order: order); } - public virtual void UpdateHUD(Character character, float deltaTime, Camera cam) { } + public void UpdateHUD(Character character, float deltaTime, Camera cam) + { + UpdateHUDComponentSpecific(character, deltaTime, cam); + if (guiFrameUpdatePending && !PlayerInput.PrimaryMouseButtonHeld()) + { + //send a guiframe position update once the player stops dragging the frame + guiFrameUpdatePending = false; + if (SerializableProperties.TryGetValue(nameof(GuiFrameOffset).ToIdentifier(), out var property)) + { + GameMain.Client?.CreateEntityEvent(Item, new Item.ChangePropertyEventData(property, this)); + } + } + } + + public virtual void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { } public virtual void UpdateEditing(float deltaTime) { } @@ -572,6 +588,7 @@ namespace Barotrauma.Items.Components } string style = GuiFrameSource.Attribute("style") == null ? null : GuiFrameSource.GetAttributeString("style", ""); GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas, Anchor.Center), style, color); + GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset; TryCreateDragHandle(); @@ -583,24 +600,25 @@ namespace Barotrauma.Items.Components GameMain.Instance.ResolutionChanged += OnResolutionChangedPrivate; } - protected virtual void TryCreateDragHandle() + protected void TryCreateDragHandle() { if (GuiFrame != null && GuiFrameSource.GetAttributeBool("draggable", true)) { bool hideDragIcons = GuiFrameSource.GetAttributeBool("hidedragicons", false); - var handle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), + guiFrameDragHandle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), GuiFrame.RectTransform, style: null) { + Enabled = !LockGuiFramePosition, DragArea = HUDLayoutSettings.ItemHUDArea }; int iconHeight = GUIStyle.ItemFrameMargin.Y / 4; - var dragIcon = new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), handle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, + var dragIcon = new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), guiFrameDragHandle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, style: "GUIDragIndicatorHorizontal"); dragIcon.RectTransform.MinSize = new Point(0, iconHeight); - handle.ValidatePosition = (RectTransform rectT) => + guiFrameDragHandle.ValidatePosition = (RectTransform rectT) => { var activeHuds = Character.Controlled?.SelectedItem?.ActiveHUDs ?? item.ActiveHUDs; foreach (ItemComponent ic in activeHuds) @@ -624,11 +642,13 @@ namespace Barotrauma.Items.Components //refresh slots to ensure they're rendered at the correct position (ic as ItemContainer)?.Inventory.CreateSlots(); } + GuiFrameOffset = GuiFrame.RectTransform.ScreenSpaceOffset; + guiFrameUpdatePending = true; return true; }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); - var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), guiFrameDragHandle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, style: "GUIButtonSettings") { OnClicked = (btn, userdata) => @@ -636,6 +656,14 @@ namespace Barotrauma.Items.Components GUIContextMenu.CreateContextMenu( new ContextMenuOption("item.resetuiposition", isEnabled: true, onSelected: () => { + foreach (var ic in item.Components) + { + if (ic.GuiFrame != null && ic.GuiFrameOffset != Point.Zero) + { + ic.GuiFrameOffset = Point.Zero; + ic.guiFrameUpdatePending = true; + } + } if (Character.Controlled?.SelectedItem != null && item != Character.Controlled.SelectedItem) { Character.Controlled.SelectedItem.ForceHUDLayoutUpdate(ignoreLocking: true); @@ -648,7 +676,11 @@ namespace Barotrauma.Items.Components new ContextMenuOption(TextManager.Get(LockGuiFramePosition ? "item.unlockuiposition" : "item.lockuiposition"), isEnabled: true, onSelected: () => { LockGuiFramePosition = !LockGuiFramePosition; - handle.Enabled = !LockGuiFramePosition; + guiFrameDragHandle.Enabled = !LockGuiFramePosition; + if (SerializableProperties.TryGetValue(nameof(LockGuiFramePosition).ToIdentifier(), out var property)) + { + GameMain.Client?.CreateEntityEvent(Item, new Item.ChangePropertyEventData(property, this)); + } })); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 17293bf7f..830703d01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; +using System.Collections.Generic; using System.Linq; -using static Barotrauma.Inventory; namespace Barotrauma.Items.Components { @@ -97,6 +97,8 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(ContentXElement element) { slotIcons = new Sprite[capacity]; + + int currCapacity = MainContainerCapacity; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -127,6 +129,19 @@ namespace Barotrauma.Items.Components } } break; + case "subcontainer": + int subContainerCapacity = subElement.GetAttributeInt("capacity", 1); + var slotIconElement = subElement.GetChildElement("sloticon"); + if (slotIconElement != null) + { + var slotIcon = new Sprite(slotIconElement); + for (int i = currCapacity; i < currCapacity + subContainerCapacity; i++) + { + slotIcons[i] = slotIcon; + } + } + currCapacity += subContainerCapacity; + break; } } @@ -180,11 +195,40 @@ namespace Barotrauma.Items.Components }; LocalizedString labelText = GetUILabel(); - GUITextBlock label = null; - if (!labelText.IsNullOrEmpty()) + GUITextBlock label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter), + labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft, wrap: true) + { + IgnoreLayoutGroups = true + }; + + int buttonSize = GUIStyle.ItemFrameTopBarHeight; + Point margin = new Point(buttonSize / 4, buttonSize / 6); + + GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(GuiFrame.Rect.Width - margin.X * 2, buttonSize - margin.Y * 2), GuiFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, margin.Y) }, + isHorizontal: true, childAnchor: Anchor.TopRight) { - label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter), - labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + AbsoluteSpacing = margin.X / 2 + }; + if (Inventory.Capacity > 1) + { + new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "SortItemsButton") + { + ToolTip = TextManager.Get("SortItemsAlphabetically"), + OnClicked = (btn, userdata) => + { + SortItems(); + return true; + } + }; + new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "MergeStacksButton") + { + ToolTip = TextManager.Get("MergeItemStacks"), + OnClicked = (btn, userdata) => + { + MergeStacks(); + return true; + } + }; } float minInventoryAreaSize = 0.5f; @@ -215,6 +259,71 @@ namespace Barotrauma.Items.Components Inventory.RectTransform = guiCustomComponent.RectTransform; } + private void SortItems() + { + List> itemsPerSlot = new List>(); + + for (int i = 0; i < Inventory.Capacity; i++) + { + var items = Inventory.GetItemsAt(i).ToList(); + if (items.Any()) + { + itemsPerSlot.Add(items); + items.ForEach(it => it.Drop(dropper: null, createNetworkEvent: false, setTransform: false)); + } + } + + itemsPerSlot.Sort((i1, i2) => i1.First().Name.CompareTo(i2.First().Name)); + foreach (var items in itemsPerSlot) + { + int firstFreeSlot = -1; + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.GetItemAt(i) == null && Inventory.CanBePut(items.First())) + { + firstFreeSlot = i; + break; + } + } + if (firstFreeSlot == -1) + { + items.ForEach(it => it.Drop(dropper: null)); + continue; + } + foreach (var item in items) + { + if (!Inventory.TryPutItem(item, firstFreeSlot, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + //if putting in the specific slot fails (prevented by containable restrictions?), just put in the first free slot + if (!Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) + { + item.Drop(dropper: null); + } + } + } + } + Inventory.CreateNetworkEvent(); + } + + private void MergeStacks() + { + for (int i = Inventory.Capacity - 1; i >= 0; i--) + { + var items = Inventory.GetItemsAt(i).ToList(); + if (items.None()) { continue; } + //find the first stack we can put the item in + for (int j = 0; j < i; j++) + { + if (Inventory.GetItemsAt(j).Any() && Inventory.CanBePutInSlot(items.First(), j)) + { + items.ForEach(it => Inventory.TryPutItem(it, j, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)); + break; + } + } + } + Inventory.CreateNetworkEvent(); + } + public LocalizedString GetUILabel() { if (UILabel == string.Empty) { return string.Empty; } @@ -277,7 +386,7 @@ namespace Barotrauma.Items.Components int ignoredItemCount = 0; var subContainableItems = AllSubContainableItems; - float targetSlotCapacity = GetMaxStackSize(targetSlot); + float targetSlotCapacity = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot)); float capacity = targetSlotCapacity * MainContainerCapacity; if (subContainableItems != null) { @@ -310,29 +419,36 @@ namespace Barotrauma.Items.Components int itemCount = Inventory.AllItems.Count() - ignoredItemCount; return Math.Min(itemCount / Math.Max(capacity, 1), 1); } + + //display the state of an item in a specific slot + if (Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1) + { + if (containedItem == null) { return 0.0f; } + //if the contained item has some contained state indicator, show that + if (containedItem.GetComponent() is { ShowContainedStateIndicator: true } containedItemContainer) + { + return containedItemContainer.GetContainedIndicatorState(); + } + int maxStackSize = Math.Min(containedItem.Prefab.GetMaxStackSize(Inventory), GetMaxStackSize(targetSlot)); + if (maxStackSize == 1) + { + return containedItem.Condition / containedItem.MaxCondition; + } + return containedItems.Count() / (float)maxStackSize; + } else { - if (containedItem != null && (Inventory.Capacity == 1 || HasSubContainers)) - { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot)); - if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) - { - return containedItems.Count() / (float)maxStackSize; - } - } - return Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1 ? - (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : - Inventory.EmptySlotCount / (float)Inventory.Capacity; - } + return Inventory.EmptySlotCount / (float)Inventory.Capacity; + } } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } - DrawContainedItems(spriteBatch, itemDepth); + DrawContainedItems(spriteBatch, itemDepth, overrideColor); } - public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth) + public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth, Color? overrideColor = null) { Vector2 transformedItemPos = ItemPos * item.Scale; Vector2 transformedItemInterval = ItemInterval * item.Scale; @@ -388,7 +504,7 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (DrawableContainedItem contained in drawableContainedItems) + foreach (ContainedItem contained in containedItems) { Vector2 itemPos = currentItemPos; @@ -466,7 +582,7 @@ namespace Barotrauma.Items.Components contained.Item.Sprite.Draw( spriteBatch, new Vector2(itemPos.X, -itemPos.Y), - isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true), + overrideColor ?? (isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true)), origin, -(contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), contained.Item.Scale, @@ -476,7 +592,7 @@ namespace Barotrauma.Items.Components foreach (ItemContainer ic in contained.Item.GetComponents()) { if (ic.hideItems) { continue; } - ic.DrawContainedItems(spriteBatch, containedSpriteDepth); + ic.DrawContainedItems(spriteBatch, containedSpriteDepth, overrideColor); } i++; @@ -497,7 +613,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (!item.IsInteractable(character)) { return; } if (Inventory.RectTransform != null) @@ -512,19 +628,10 @@ namespace Barotrauma.Items.Components //if the item is in the character's inventory, no need to update the item's inventory //because the player can see it by hovering the cursor over the item - guiCustomComponent.Visible = DrawInventory && item.ParentInventory?.Owner != character; + guiCustomComponent.Visible = DrawInventory && (item.ParentInventory?.Owner != character || Inventory.DrawWhenEquipped); if (!guiCustomComponent.Visible) { return; } Inventory.Update(deltaTime, cam); } - - /*public override void DrawHUD(SpriteBatch spriteBatch, Character character) - { - //if the item is in the character's inventory, no need to draw the item's inventory - //because the player can see it by hovering the cursor over the item - if (item.ParentInventory?.Owner == character || !DrawInventory) return; - - Inventory.Draw(spriteBatch); - }*/ } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 9b1334f3a..a6ff486bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components prevRect = item.Rect; } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (item.ParentInventory != null) { return; } if (editing) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs index 8911109d8..8fc4a144c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs @@ -19,13 +19,14 @@ namespace Barotrauma.Items.Components private Sprite backgroundSprite; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (backgroundSprite == null) { return; } backgroundSprite.DrawTiled(spriteBatch, new Vector2(item.DrawPosition.X - item.Rect.Width / 2 * item.Scale, -(item.DrawPosition.Y + item.Rect.Height / 2)) - backgroundSprite.Origin * item.Scale, - new Vector2(backgroundSprite.size.X * item.Scale, item.Rect.Height), color: item.Color, + new Vector2(backgroundSprite.size.X * item.Scale, item.Rect.Height), + color: overrideColor ?? item.Color, textureScale: Vector2.One * item.Scale, depth: BackgroundSpriteDepth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 763d89b4f..540bc1759 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (Light?.LightSprite == null) { return; } if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 71cfdb1b4..ba8b62d21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -428,7 +428,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { inSufficientPowerWarning.Visible = IsActive && !hasPower; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 745eca08b..8c3d920c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { powerIndicator.Selected = hasPower && IsActive; autoControlIndicator.Selected = controlLockTimer > 0.0f; @@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (propellerSprite != null) { Vector2 drawPos = item.DrawPosition; drawPos += PropellerPos; drawPos.Y = -drawPos.Y; - propellerSprite.Draw(spriteBatch, (int)Math.Floor(spriteIndex), drawPos, Color.White, propellerSprite.Origin, 0.0f, Vector2.One); + propellerSprite.Draw(spriteBatch, (int)Math.Floor(spriteIndex), drawPos, overrideColor ?? Color.White, propellerSprite.Origin, 0.0f, Vector2.One); } if (editing && !DisablePropellerDamage && propellerDamage != null && !GUI.DisableHUD) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3ac43ec4a..c86d674e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -38,6 +38,11 @@ namespace Barotrauma.Items.Components } private FabricationRecipe selectedItem; + /// + /// Which character's skills the current view is displayed based on + /// + private Character displayingForCharacter; + public Identifier SelectedItemIdentifier => SelectedItem?.TargetItem.Identifier ?? Identifier.Empty; private GUIComponent inSufficientPowerWarning; @@ -358,9 +363,11 @@ namespace Barotrauma.Items.Components if (inputInventoryHolder != null) { inputContainer.AllowUIOverlap = true; + inputContainer.Inventory.DrawWhenEquipped = true; inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; } outputContainer.AllowUIOverlap = true; + outputContainer.Inventory.DrawWhenEquipped = true; outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; } @@ -453,13 +460,8 @@ namespace Barotrauma.Items.Components requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform)); } + FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty); HideEmptyItemListCategories(); - - if (selectedItem != null) - { - //reselect to recreate the info based on the new user's skills - SelectItem(character, selectedItem); - } } private readonly Dictionary missingIngredientCounts = new Dictionary(); @@ -538,6 +540,8 @@ namespace Barotrauma.Items.Components int slotIndex = 0; foreach (var kvp in missingIngredientCounts) { + if (inputContainer.Inventory?.visualSlots == null) { break; } + var requiredItem = kvp.Key; int missingCount = kvp.Value; @@ -560,11 +564,10 @@ namespace Barotrauma.Items.Components var requiredItemPrefab = requiredItem.FirstMatchingPrefab; float iconAlpha = 0.0f; - ItemPrefab requiredItemToDisplay; - int count = requiredItem.ItemPrefabs.Count(); - if (count > 1) + ItemPrefab requiredItemToDisplay = requiredItem.DefaultItem.IsEmpty ? null : requiredItem.ItemPrefabs.FirstOrDefault(p => p.Identifier == requiredItem.DefaultItem); + if (requiredItemToDisplay == null && requiredItem.ItemPrefabs.Multiple()) { - float iconCycleSpeed = 0.5f / count; + float iconCycleSpeed = 0.75f; float iconCycleT = (float)Timing.TotalTime * iconCycleSpeed; int iconIndex = (int)(iconCycleT % requiredItem.ItemPrefabs.Count()); @@ -573,7 +576,7 @@ namespace Barotrauma.Items.Components } else { - requiredItemToDisplay = requiredItem.ItemPrefabs.FirstOrDefault(); + requiredItemToDisplay ??= requiredItem.ItemPrefabs.FirstOrDefault(); iconAlpha = 1.0f; } if (iconAlpha > 0.0f) @@ -616,9 +619,12 @@ namespace Barotrauma.Items.Components if (slotRect.Contains(PlayerInput.MousePosition)) { - var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name).Distinct(); - LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); - if (suitableIngredients.Count() > 3) { toolTipText += "..."; } + LocalizedString toolTipText = requiredItem.OverrideHeader; + if (requiredItem.OverrideHeader.IsNullOrEmpty()) + { + var suitableIngredients = requiredItem.ItemPrefabs.Where(ip => !ip.HideInMenus).OrderBy(ip => ip.DefaultPrice?.Price ?? 0).Select(ip => ip.Name).Distinct(); + toolTipText = GetSuitableIngredientText(suitableIngredients); + } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; @@ -656,15 +662,68 @@ namespace Barotrauma.Items.Components } } + private LocalizedString GetSuitableIngredientText(IEnumerable itemNameList) + { + int count = itemNameList.Count(); + if (count == 0) + { + return string.Empty; + } + else if (count == 1) + { + return itemNameList.First(); + } + else if (count == 2) + { + //[item1] or [item2] + return TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsLast", + ("[treatment1]", itemNameList.ElementAt(0)), + ("[treatment2]", itemNameList.ElementAt(1))); + } + else + { + // [item1], [item2], [item3] ... or [lastitem] + LocalizedString itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsFirst", + ("[treatment1]", itemNameList.ElementAt(0)), + ("[treatment2]", itemNameList.ElementAt(1))); + + int i; + bool isTruncated = false; + for (i = 2; i < count - 1; i++) + { + if (itemListStr.Length > 50) + { + isTruncated = true; + break; + } + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsFirst", + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList.ElementAt(i))); + } + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsLast", + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList.ElementAt(i))); + + if (isTruncated) + { + itemListStr += TextManager.Get("ellipsis"); + } + return itemListStr; + } + } + private void DrawOutputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); FabricationRecipe targetItem = fabricatedItem ?? selectedItem; - if (targetItem != null) + if (targetItem != null && outputContainer.Inventory?.visualSlots != null) { Rectangle slotRect = outputContainer.Inventory.visualSlots[0].Rect; - if (fabricatedItem != null) { float clampedProgressState = Math.Clamp(progressState, 0f, 1f); @@ -699,6 +758,16 @@ namespace Barotrauma.Items.Components { FabricationRecipe recipe = child.UserData as FabricationRecipe; if (recipe?.DisplayName == null) { continue; } + + if (recipe.HideForNonTraitors) + { + if (Character.Controlled is not { IsTraitor: true }) + { + child.Visible = false; + continue; + } + } + child.Visible = (string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) && (!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value)); @@ -749,8 +818,9 @@ namespace Barotrauma.Items.Components private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null) { this.selectedItem = selectedItem; + displayingForCharacter = user; - int max = Math.Max(selectedItem.TargetItem.MaxStackSize / selectedItem.Amount, 1); + int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1); if (amountInput != null) { @@ -924,7 +994,7 @@ namespace Barotrauma.Items.Components return true; } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { activateButton.Enabled = false; inSufficientPowerWarning.Visible = IsActive && !hasPower; @@ -933,6 +1003,12 @@ namespace Barotrauma.Items.Components if (!IsActive) { + if (selectedItem != null && displayingForCharacter != character) + { + //reselect to recreate the info based on the new user's skills + SelectItem(character, selectedItem); + } + //only check ingredients if the fabricator isn't active (if it is, this is done in Update) if (refreshIngredientsTimer <= 0.0f) { @@ -998,9 +1074,13 @@ namespace Barotrauma.Items.Components { fabricationLimits[msg.ReadUInt32()] = 0; } - State = newState; - this.amountToFabricate = amountToFabricate; + //don't touch the amount unless another character changed it or the fabricator is running + //otherwise we may end up reverting the changes the client just did to the amount + if ((user != null && user != Character.Controlled) || State != FabricatorState.Stopped) + { + this.amountToFabricate = amountToFabricate; + } this.amountRemaining = amountRemaining; if (newState == FabricatorState.Stopped || recipeHash == 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 6759ec985..b81560ca1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -295,7 +295,7 @@ namespace Barotrauma.Items.Components } } - OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsVisibleAsReportButton).OrderBy(o => o.Identifier).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -350,10 +350,16 @@ namespace Barotrauma.Items.Components } }; + List shownItemPrefabs = new List(); foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(prefab => prefab.Name)) { if (prefab.HideInMenus) { continue; } + if (shownItemPrefabs.Any(ip => DisplayAsSameItem(ip, prefab))) + { + continue; + } CreateItemFrame(prefab, listBox.Content.RectTransform); + shownItemPrefabs.Add(prefab); } searchBar.OnDeselected += (sender, key) => @@ -398,6 +404,27 @@ namespace Barotrauma.Items.Components new Point(int.MaxValue, paddedContainer.Rect.Height - bottomFrame.Rect.Height - buttonLayout.Rect.Height); } + private static Sprite GetPreviewSprite(ItemPrefab prefab) + { + return prefab.InventoryIcon ?? prefab.Sprite; + } + + /// + /// If the items have an identical name and icon (e.g. a variant with an alternative fabrication/deconstruction recipe), + /// they're displayed as if they were the same item, not as two separate entries. + /// + private static bool DisplayAsSameItem(ItemPrefab prefab1, ItemPrefab prefab2) + { + if (prefab1 == prefab2) { return true; } + if (prefab1.Name == prefab2.Name) + { + var sprite1 = GetPreviewSprite(prefab1); + var sprite2 = GetPreviewSprite(prefab2); + return sprite1?.FullPath == sprite2?.FullPath && sprite1?.SourceRect == sprite2?.SourceRect; + } + return false; + } + private bool VisibleOnItemFinder(Item it) { if (it?.Submarine == null) { return false; } @@ -413,7 +440,7 @@ namespace Barotrauma.Items.Components if (it.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } - if (it.HasTag("traitormissionitem")) { return false; } + if (it.HasTag(Tags.TraitorMissionItem)) { return false; } return true; } @@ -539,13 +566,13 @@ namespace Barotrauma.Items.Components displayedSubs.Add(item.Submarine); displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID)); - subEntities = MapEntity.mapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); + subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); BakeSubmarine(item.Submarine, parentRect); elementSize = GuiFrame.Rect.Size; } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs @@ -824,7 +851,8 @@ namespace Barotrauma.Items.Components foreach (GUIComponent component in listBox.Content.Children) { component.Visible = false; - if (component.UserData is ItemPrefab { Name: { } prefabName} prefab && itemsFoundOnSub.Contains(prefab)) + if (component.UserData is ItemPrefab { Name: { } prefabName} prefab && + (itemsFoundOnSub.Contains(prefab) || itemsFoundOnSub.Any(ip => DisplayAsSameItem(ip, prefab)))) { component.Visible = prefabName.ToLower().Contains(text.ToLower()); @@ -851,9 +879,9 @@ namespace Barotrauma.Items.Components tooltip.RectTransform.ScreenSpaceOffset = new Point(box.Rect.X, box.Rect.Y - height); } - private void CreateItemFrame(ItemPrefab prefab, RectTransform parent) + private static void CreateItemFrame(ItemPrefab prefab, RectTransform parent) { - Sprite sprite = prefab.InventoryIcon ?? prefab.Sprite; + Sprite sprite = GetPreviewSprite(prefab); if (sprite is null) { return; } GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement") { @@ -899,7 +927,7 @@ namespace Barotrauma.Items.Components { if (!VisibleOnItemFinder(it)) { continue; } - if (it.Prefab == searchedPrefab) + if (DisplayAsSameItem(it.Prefab, searchedPrefab)) { // ignore items on players and hidden inventories if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { HiddenInGame: true }}) is { }) { continue; } @@ -1079,7 +1107,7 @@ namespace Barotrauma.Items.Components if (ShowHullIntegrity) { float amount = 1f + hullData.LinkedHulls.Count; - gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom && !g.HiddenInGame).Sum(g => g.Open) / amount; + gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.HiddenInGame).Sum(g => g.Open) / amount; borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index d3076f105..2c0adbcb6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components private float flickerTimer; private readonly float flickerFrequency = 1; - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { autoControlIndicator.Selected = IsAutoControlled; PowerButton.Enabled = isActiveLockTimer <= 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 56194db32..43ec63ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -615,7 +615,7 @@ namespace Barotrauma.Items.Components turbineOutputMeter, TurbineOutput, new Vector2(0.0f, 100.0f), clampedOptimalTurbineOutput, clampedAllowedTurbineOutput); } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { IsActive = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 0f61141c9..e06de7a92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -109,7 +109,7 @@ namespace Barotrauma.Items.Components }, { BlipType.Destructible, - new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } + new Color[] { Color.TransparentBlack, new Color(94, 114, 73) * 0.8f, new Color(255, 236, 151) * 0.8f, new Color(242, 243, 194) * 0.8f } }, { BlipType.Door, @@ -358,11 +358,6 @@ namespace Barotrauma.Items.Components } } - protected override void TryCreateDragHandle() - { - base.TryCreateDragHandle(); - } - private void SetPingDirection(Vector2 direction) { pingDirection = direction; @@ -471,7 +466,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { showDirectionalIndicatorTimer -= deltaTime; if (GameMain.Client != null) @@ -981,38 +976,41 @@ namespace Barotrauma.Items.Components } } - if (GameMain.GameSession == null || Level.Loaded == null) { return; } + if (GameMain.GameSession == null) { return; } - if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) + if (Level.Loaded != null) { - DrawMarker(spriteBatch, - Level.Loaded.StartLocation.Name, - (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), - "startlocation", - Level.Loaded.StartExitPosition, transducerCenter, - displayScale, center, DisplayRadius); - } + if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) + { + DrawMarker(spriteBatch, + Level.Loaded.StartLocation.Name, + (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), + "startlocation", + Level.Loaded.StartExitPosition, transducerCenter, + displayScale, center, DisplayRadius); + } - if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) - { - DrawMarker(spriteBatch, - Level.Loaded.EndLocation.Name, - (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), - "endlocation", - Level.Loaded.EndExitPosition, transducerCenter, - displayScale, center, DisplayRadius); - } + if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) + { + DrawMarker(spriteBatch, + Level.Loaded.EndLocation.Name, + (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), + "endlocation", + Level.Loaded.EndExitPosition, transducerCenter, + displayScale, center, DisplayRadius); + } - for (int i = 0; i < Level.Loaded.Caves.Count; i++) - { - var cave = Level.Loaded.Caves[i]; - if (!cave.DisplayOnSonar) { continue; } - DrawMarker(spriteBatch, - caveLabel.Value, - "cave".ToIdentifier(), - "cave" + i, - cave.StartPos.ToVector2(), transducerCenter, - displayScale, center, DisplayRadius); + for (int i = 0; i < Level.Loaded.Caves.Count; i++) + { + var cave = Level.Loaded.Caves[i]; + if (cave.MissionsToDisplayOnSonar.None()) { continue; } + DrawMarker(spriteBatch, + caveLabel.Value, + "cave".ToIdentifier(), + "cave" + i, + cave.StartPos.ToVector2(), transducerCenter, + displayScale, center, DisplayRadius); + } } int missionIndex = 0; @@ -1070,7 +1068,7 @@ namespace Barotrauma.Items.Components { if (!sub.ShowSonarMarker) { continue; } if (connectedSubs.Contains(sub)) { continue; } - if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } if (item.Submarine != null || Character.Controlled != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 6a5d4a44b..fd5be2193 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -532,6 +532,22 @@ namespace Barotrauma.Items.Components }; } + /// + /// Map the rectangular steering vector to a circular area using FG-Squircular Mapping which preserves the angle of the vector. + /// + private static Vector2 MapSquareToCircle(Vector2 steeringVector) + { + float xSqr = steeringVector.X * steeringVector.X; + float ySqr = steeringVector.Y * steeringVector.Y; + float length = MathF.Sqrt(ySqr + xSqr); + if (MathUtils.NearlyEqual(length, 0.0f)) { return Vector2.Zero; } + + //FG-Squircular mapping formula from https://arxiv.org/ftp/arxiv/papers/1509/1509.06344.pdf + float x = steeringVector.X * MathF.Sqrt(xSqr + ySqr - xSqr * ySqr) / length; + float y = steeringVector.Y * MathF.Sqrt(xSqr + ySqr - xSqr * ySqr) / length; + return new Vector2(x, y); + } + public void DrawHUD(SpriteBatch spriteBatch, Rectangle rect) { int width = rect.Width, height = rect.Height; @@ -545,11 +561,9 @@ namespace Barotrauma.Items.Components if (!AutoPilot) { - Vector2 unitSteeringInput = steeringInput / 100.0f; //map input from rectangle to circle - Vector2 steeringInputPos = new Vector2( - steeringInput.X * (float)Math.Sqrt(1.0f - 0.5f * unitSteeringInput.Y * unitSteeringInput.Y), - -steeringInput.Y * (float)Math.Sqrt(1.0f - 0.5f * unitSteeringInput.X * unitSteeringInput.X)); + Vector2 steeringInputPos = MapSquareToCircle(steeringInput / 100f) * 100.0f; + steeringInputPos.Y = -steeringInputPos.Y; steeringInputPos += steeringOrigin; if (steeringIndicator != null) @@ -604,10 +618,8 @@ namespace Barotrauma.Items.Components } //map velocity from rectangle to circle - Vector2 unitTargetVel = targetVelocity / 100.0f; - Vector2 steeringPos = new Vector2( - targetVelocity.X * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.Y * unitTargetVel.Y), - -targetVelocity.Y * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.X * unitTargetVel.X)); + Vector2 steeringPos = MapSquareToCircle(targetVelocity / 100f) * 90.0f; + steeringPos.Y = -steeringPos.Y; steeringPos += steeringOrigin; if (steeringIndicator != null) @@ -682,7 +694,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (swapDestinationOrder == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs index c0d347358..d91e5966b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { for (var i = 0; i < GrowableSeeds.Length; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 13e9d9ffc..d2328aac7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -122,7 +122,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (chargeIndicator != null) { @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { Vector2 scaledIndicatorSize = indicatorSize * item.Scale; if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs index 0a1cbd605..cd4053924 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs @@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components private GUITickBox highVoltageIndicator; private GUITickBox lowVoltageIndicator; + private GUITextBlock powerLabel, loadLabel; + + private LanguageIdentifier prevLanguage; + partial void InitProjectSpecific(XElement element) { if (GuiFrame == null) { return; } @@ -56,12 +60,12 @@ namespace Barotrauma.Items.Components Stretch = true }; - var powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), + powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), TextManager.Get("PowerTransferPowerLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipPower") }; - var loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform), + loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform), TextManager.Get("PowerTransferLoadLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipLoad") @@ -75,7 +79,7 @@ namespace Barotrauma.Items.Components ToolTip = TextManager.Get("PowerTransferTipPower"), TextGetter = () => { float currPower = powerLoad < 0 ? -powerLoad: 0; - if (!(this is RelayComponent) && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null) + if (this is not RelayComponent && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null) { currPower = PowerConnections[0].Grid.Power; } @@ -119,9 +123,11 @@ namespace Barotrauma.Items.Components GUITextBlock.AutoScaleAndNormalize(powerLabel, loadLabel); GUITextBlock.AutoScaleAndNormalize(true, true, powerText, loadText); GUITextBlock.AutoScaleAndNormalize(kw1, kw2); + + prevLanguage = GameSettings.CurrentConfig.Language; } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (GuiFrame == null) return; @@ -129,6 +135,13 @@ namespace Barotrauma.Items.Components powerIndicator.Selected = IsActive && voltage > 0; highVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage > 1.2f; lowVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage < 0.8f; + + if (prevLanguage != GameSettings.CurrentConfig.Language) + { + GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, highVoltageIndicator.TextBlock, lowVoltageIndicator.TextBlock); + GUITextBlock.AutoScaleAndNormalize(powerLabel, loadLabel); + prevLanguage = GameSettings.CurrentConfig.Language; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index aaca8fda0..7993a8a97 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -42,59 +42,98 @@ namespace Barotrauma.Items.Components Vector2 axis = new Vector2( msg.ReadSingle(), msg.ReadSingle()); - UInt16 entityID = msg.ReadUInt16(); + StickTargetType targetType = (StickTargetType)msg.ReadByte(); - Entity entity = Entity.FindEntityByID(entityID); Submarine submarine = Entity.FindEntityByID(submarineID) as Submarine; - Hull hull = Entity.FindEntityByID(hullID) as Hull; + Hull hull = Entity.FindEntityByID(hullID) as Hull; item.Submarine = submarine; item.CurrentHull = hull; item.body.SetTransform(simPosition, item.body.Rotation); - if (entity is Character character) + + switch (targetType) { - byte limbIndex = msg.ReadByte(); - if (limbIndex >= character.AnimController.Limbs.Length) - { - DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character.ToString()})"); - return; - } - if (character.Removed) { return; } - var limb = character.AnimController.Limbs[limbIndex]; - StickToTarget(limb.body.FarseerBody, axis); - } - else if (entity is Structure structure) - { - byte bodyIndex = msg.ReadByte(); - if (bodyIndex == 255) { bodyIndex = 0; } - if (bodyIndex >= structure.Bodies.Count) - { - DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure.ToString()})"); - return; - } - var body = structure.Bodies[bodyIndex]; - StickToTarget(body, axis); - } - else if (entity is Item item) - { - if (item.Removed) { return; } - var door = item.GetComponent(); - if (door != null) - { - StickToTarget(door.Body.FarseerBody, axis); - } - else if (item.body != null) - { - StickToTarget(item.body.FarseerBody, axis); - } - } - else if (entity is Submarine sub) - { - StickToTarget(sub.PhysicsBody.FarseerBody, axis); - } - else - { - DebugConsole.ThrowError($"Failed to read a projectile update from the server. Invalid stick target ({entity?.ToString() ?? "null"}, {entityID})"); - } + case StickTargetType.Structure: + UInt16 structureId = msg.ReadUInt16(); + byte bodyIndex = msg.ReadByte(); + if (Entity.FindEntityByID(structureId) is Structure structure) + { + if (bodyIndex == 255) { bodyIndex = 0; } + if (bodyIndex >= structure.Bodies.Count) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure})"); + return; + } + var body = structure.Bodies[bodyIndex]; + StickToTarget(body, axis); + } + else + { + DebugConsole.AddWarning($"\"{item.Prefab.Identifier}\" failed to stick to a structure. Could not find a structure with the ID {structureId}"); + } + break; + case StickTargetType.Limb: + UInt16 characterId = msg.ReadUInt16(); + byte limbIndex = msg.ReadByte(); + if (Entity.FindEntityByID(characterId) is Character character) + { + if (limbIndex >= character.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character})"); + return; + } + if (character.Removed) { return; } + var limb = character.AnimController.Limbs[limbIndex]; + StickToTarget(limb.body.FarseerBody, axis); + } + else + { + DebugConsole.AddWarning($"\"{this.item.Prefab.Identifier}\" failed to stick to a limb. Could not find a character with the ID {characterId}"); + } + break; + case StickTargetType.Item: + UInt16 itemID = msg.ReadUInt16(); + if (Entity.FindEntityByID(itemID) is Item targetItem) + { + if (targetItem.Removed) { return; } + var door = targetItem.GetComponent(); + if (door != null) + { + StickToTarget(door.Body.FarseerBody, axis); + } + else if (targetItem.body != null) + { + StickToTarget(targetItem.body.FarseerBody, axis); + } + } + else + { + DebugConsole.AddWarning($"\"{this.item.Prefab.Identifier}\" failed to stick to an item. Could not find n item with the ID {itemID}"); + } + break; + case StickTargetType.Submarine: + UInt16 targetSubmarineId = msg.ReadUInt16(); + if (Entity.FindEntityByID(targetSubmarineId) is Submarine targetSub) + { + StickToTarget(targetSub.PhysicsBody.FarseerBody, axis); + } + else + { + DebugConsole.AddWarning($"\"{item.Prefab.Identifier}\" failed to stick to a submarine. Could not find a structure with the ID {targetSubmarineId}"); + } + break; + case StickTargetType.LevelWall: + int levelWallIndex = msg.ReadInt32(); + var allCells = Level.Loaded.GetAllCells(); + if (levelWallIndex >= 0 && levelWallIndex < allCells.Count) + { + StickToTarget(allCells[levelWallIndex].Body, axis); + } + else + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Level wall index out of bounds ({levelWallIndex}, wall count: {allCells.Count})"); + } + break; + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs index 6f6d3d740..31f951899 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components currentTarget?.DrawHUD(spriteBatch, Screen.Selected.Cam, character); } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { currentTarget?.UpdateHUD(cam, character,deltaTime); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index a449dc00f..8f58322ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -144,7 +144,7 @@ namespace Barotrauma.Items.Components } } #if DEBUG - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (GameMain.DebugDraw && IsActive) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index c25a7c6cc..59010730c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -373,12 +373,19 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (GameMain.DebugDraw && Character.Controlled?.FocusedItem == item) { - bool paused = !ShouldDeteriorate(); - if (deteriorationTimer > 0.0f) + bool paused = !ShouldDeteriorate() && ForceDeteriorationTimer <= 0.0f; + if (ForceDeteriorationTimer > 0.0f) + { + GUI.DrawString(spriteBatch, + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Forced deterioration for " + ((int)ForceDeteriorationTimer) + " s", + Color.Red, Color.Black * 0.5f); + + } + else if (deteriorationTimer > 0.0f) { GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Deterioration delay " + ((int)deteriorationTimer) + (paused ? " [PAUSED]" : ""), @@ -430,8 +437,7 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { deteriorationTimer = msg.ReadSingle(); - deteriorateAlwaysResetTimer = msg.ReadSingle(); - DeteriorateAlways = msg.ReadBoolean(); + ForceDeteriorationTimer = msg.ReadSingle(); tinkeringDuration = msg.ReadSingle(); tinkeringStrength = msg.ReadSingle(); tinkeringPowersDevices = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 5bee8bba0..79adeb549 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -55,20 +55,6 @@ namespace Barotrauma.Items.Components } } - private Vector2 GetSourcePos() - { - Vector2 sourcePos = source.WorldPosition; - if (source is Item sourceItem) - { - sourcePos = sourceItem.DrawPosition; - } - else if (source is Limb sourceLimb && sourceLimb.body != null) - { - sourcePos = sourceLimb.body.DrawPosition; - } - return sourcePos; - } - partial void InitProjSpecific(ContentXElement element) { foreach (var subElement in element.Elements()) @@ -88,34 +74,22 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (target == null || target.Removed) { return; } if (target.ParentInventory != null) { return; } if (source is Limb limb && limb.Removed) { return; } if (source is Entity e && e.Removed) { return; } - Vector2 startPos = GetSourcePos(); + Vector2 startPos = GetSourcePos(useDrawPosition: true); startPos.Y = -startPos.Y; - if (source is Item sourceItem && !sourceItem.Removed) + if ((source as Item)?.GetComponent() is { } turret) { - var turret = sourceItem.GetComponent(); - var weapon = sourceItem.GetComponent(); - if (turret != null) + if (turret.BarrelSprite != null) { - startPos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, -(sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y)); - if (turret.BarrelSprite != null) - { - startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; - } - startPos -= turret.GetRecoilOffset(); - } - else if (weapon != null) - { - Vector2 barrelPos = FarseerPhysics.ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); - barrelPos.Y = -barrelPos.Y; - startPos += barrelPos; + startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } + startPos -= turret.GetRecoilOffset(); } Vector2 endPos = new Vector2(target.DrawPosition.X, target.DrawPosition.Y); Vector2 flippedPos = target.Sprite.size * target.Scale * (Origin - new Vector2(0.5f)); @@ -156,28 +130,28 @@ namespace Barotrauma.Items.Components if (startSprite != null) { float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); - startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); + startSprite?.Draw(spriteBatch, startPos, overrideColor ?? SpriteColor, angle, depth: depth); } if (endSprite != null && (!Snapped || BreakFromMiddle)) { float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); - endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); + endSprite?.Draw(spriteBatch, endPos, overrideColor ?? SpriteColor, angle, depth: depth); } } } - private void DrawRope(SpriteBatch spriteBatch, Vector2 startPos, Vector2 endPos, int width) + private void DrawRope(SpriteBatch spriteBatch, Vector2 startPos, Vector2 endPos, int width, Color? overrideColor = null) { float depth = sprite == null ? item.Sprite.Depth + 0.001f : Math.Min(item.GetDrawDepth() + (sprite.Depth - item.Sprite.Depth), 0.999f); - + if (sprite?.Texture == null) { GUI.DrawLine(spriteBatch, startPos, endPos, - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); return; } @@ -191,7 +165,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, sprite, startPos + dir * (x - 5.0f), startPos + dir * (x + sprite.size.X), - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); } float leftOver = length - x; if (leftOver > 0.0f) @@ -199,7 +173,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, sprite, startPos + dir * (x - 5.0f), endPos, - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); } } else @@ -207,7 +181,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, sprite, startPos, endPos, - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs new file mode 100644 index 000000000..7dafcf617 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -0,0 +1,499 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal sealed partial class CircuitBox + { + public CircuitBoxUI? UI; + public readonly Dictionary ActiveCursors = new Dictionary(); + public Option HeldComponent = Option.None; + + private const float CursorUpdateInterval = 1f; + private float cursorUpdateTimer; + + private readonly Vector2[] recordedCursorPositions = new Vector2[10]; + private Option recordedDragStart = Option.None; + private Option recordedHeldPrefab = Option.None; + + /// + /// If the circuit box was initialized by the server instead of from the save file. + /// Used to ensure the wires the server sends are properly connected up when we load in. + /// + private bool wasInitializedByServer; + + public Sprite? WireSprite { get; private set; } + public Sprite? ConnectionSprite { get; private set; } + public Sprite? WireConnectorSprite { get; private set; } + public Sprite? ConnectionScrewSprite { get; private set; } + public UISprite? NodeFrameSprite { get; private set; } + public UISprite? NodeTopSprite { get; private set; } + + protected override void CreateGUI() + { + base.CreateGUI(); + GuiFrame.ClearChildren(); + UI?.CreateGUI(GuiFrame); + } + + partial void InitProjSpecific(ContentXElement element) + { + UI = new CircuitBoxUI(this); + IsActive = true; + CreateGUI(); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "wiresprite": + WireSprite = new Sprite(subElement); + break; + case "connectionsprite": + ConnectionSprite = new Sprite(subElement); + break; + case "wireconnectorsprite": + WireConnectorSprite = new Sprite(subElement); + break; + case "connectionscrewsprite": + ConnectionScrewSprite = new Sprite(subElement); + break; + } + } + + if (GUIStyle.GetComponentStyle("CircuitBoxTop") is { } topStyle) + { + NodeTopSprite = topStyle.Sprites[GUIComponent.ComponentState.None][0]; + } + + if (GUIStyle.GetComponentStyle("CircuitBoxFrame") is { } compStyle) + { + NodeFrameSprite = compStyle.Sprites[GUIComponent.ComponentState.None][0]; + } + } + + public override bool ShouldDrawHUD(Character character) + => character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item); + + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) + { + if (UI is null) { return; } + + UI.Update(deltaTime); + + if (GameMain.NetworkMember is null) { return; } + + foreach (var (cursorChar, cursor) in ActiveCursors) + { + if (!cursor.IsActive) { continue; } + + ActiveCursors[cursorChar].Update(deltaTime); + } + + Vector2 cursorPos = UI.GetCursorPosition(); + int lastCursorPosIndex = recordedCursorPositions.Length - 1; + + if (cursorUpdateTimer < CursorUpdateInterval) + { + cursorUpdateTimer += deltaTime; + int cursorIndex = (int)MathF.Floor(cursorUpdateTimer * lastCursorPosIndex); + RecordCursorPosition(cursorIndex); + } + else + { + RecordCursorPosition(lastCursorPosIndex); + SendCursorState(recordedCursorPositions, recordedDragStart, recordedHeldPrefab.Select(static c => c.Identifier)); + + recordedDragStart = Option.None; + recordedHeldPrefab = Option.None; + cursorUpdateTimer = 0f; + } + + void RecordCursorPosition(int index) + { + var dragStart = UI.GetDragStart(); + if (dragStart.IsSome()) { recordedDragStart = dragStart; } + + var heldComponent = HeldComponent; + if (heldComponent.IsSome()) { recordedHeldPrefab = heldComponent; } + + if (index >= 0 && index < recordedCursorPositions.Length) { recordedCursorPositions[index] = cursorPos; } + } + } + + public void RemoveComponents(IReadOnlyCollection node) + { + var ids = node.Select(static n => n.ID).ToImmutableArray(); + + if (GameMain.NetworkMember is null) + { + CreateRefundItemsForUsedResources(ids, Character.Controlled); + RemoveComponentInternal(ids); + return; + } + + if (!node.Any()) { return; } + + CreateClientEvent(new CircuitBoxRemoveComponentEvent(ids)); + } + + public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two) + { + if (GameMain.NetworkMember is null) + { + Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab); + return; + } + + if (!VerifyConnection(one, two)) { return; } + + CreateClientEvent(new CircuitBoxClientAddWireEvent(Color.White, CircuitBoxConnectorIdentifier.FromConnection(one), CircuitBoxConnectorIdentifier.FromConnection(two), CircuitBoxWire.SelectedWirePrefab.UintIdentifier)); + } + + public void RemoveWires(IReadOnlyCollection wires) + { + var ids = wires.Select(static w => w.ID).ToImmutableArray(); + if (GameMain.NetworkMember is null) + { + RemoveWireInternal(ids); + return; + } + + if (!ids.Any()) { return; } + CreateClientEvent(new CircuitBoxRemoveWireEvent(ids)); + } + + public void SelectComponents(IReadOnlyCollection moveables, bool overwrite) + { + if (Character.Controlled is not { ID: var controlledId }) { return; } + + var ids = ImmutableArray.CreateBuilder(); + var ios = ImmutableArray.CreateBuilder(); + + foreach (var moveable in moveables) + { + if (moveable is { IsSelected: true, IsSelectedByMe: false }) { continue; } + + switch (moveable) + { + case CircuitBoxComponent node: + ids.Add(node.ID); + break; + case CircuitBoxInputOutputNode io: + ios.Add(io.NodeType); + break; + } + } + + if (GameMain.NetworkMember is null) + { + SelectComponentsInternal(ids, controlledId, overwrite); + SelectInputOutputInternal(ios, controlledId, overwrite); + return; + } + + if ((!ids.Any() && !ios.Any()) && !overwrite) { return; } + + CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), overwrite, controlledId)); + } + + public void SelectWires(IReadOnlyCollection wires, bool overwrite) + { + if (Character.Controlled is not { ID: var controlledId }) { return; } + + var ids = (from wire in wires where !wire.IsSelected || wire.IsSelectedByMe select wire.ID).ToImmutableArray(); + + if (GameMain.NetworkMember is null) + { + SelectWiresInternal(ids, controlledId, overwrite); + return; + } + + if (!ids.Any() && !overwrite) { return; } + + CreateClientEvent(new CircuitBoxSelectWiresEvent(ids, overwrite, Character.Controlled.ID)); + } + + public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection moveables) + { + var ids = ImmutableArray.CreateBuilder(); + var ios = ImmutableArray.CreateBuilder(); + + foreach (CircuitBoxNode move in moveables) + { + switch (move) + { + case CircuitBoxComponent node: + ids.Add(node.ID); + break; + case CircuitBoxInputOutputNode io: + ios.Add(io.NodeType); + break; + } + } + + if (GameMain.NetworkMember is null) + { + MoveNodesInternal(ids, ios, moveAmount); + return; + } + + if (!ids.Any() && !ios.Any()) { return; } + + + CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), moveAmount)); + } + + public void AddComponent(ItemPrefab prefab, Vector2 pos) + { + if (GameMain.NetworkMember is null) + { + ItemPrefab resource; + + if (IsFull) { return; } + + if (IsInGame()) + { + if (!GetApplicableResourcePlayerHas(prefab, Character.Controlled).TryUnwrap(out var r)) { return; } + resource = r.Prefab; + RemoveItem(r); + } + else + { + resource = ItemPrefab.Prefabs[Tags.FPGACircuit]; + } + + AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, static delegate { }); + return; + } + + CreateClientEvent(new CircuitBoxAddComponentEvent(prefab.UintIdentifier, pos)); + } + + public partial void OnViewUpdateProjSpecific() + { + UI?.MouseSnapshotHandler.UpdateConnections(); + UI?.UpdateComponentList(); + } + + protected override void OnResolutionChanged() + { + base.OnResolutionChanged(); + CreateGUI(); + } + + // Remove selection when the circuit box is deselected + public partial void OnDeselected(Character c) + { + cursorUpdateTimer = 0f; + + // Server will broadcast the deselection, we don't need to do it ourselves + if (GameMain.NetworkMember is not null) { return; } + ClearAllSelectionsInternal(c.ID); + } + + public void ClientRead(INetSerializableStruct data) + { + switch (data) + { + case NetCircuitBoxCursorInfo cursorInfo: + { + ClientReadCursor(cursorInfo); + break; + } + case CircuitBoxErrorEvent errorData: + { + DebugConsole.ThrowError($"The server responded with an error: {errorData.Message}"); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(data), data, "This data cannot be handled using direct network messages."); + } + } + + public void SendMessage(CircuitBoxOpcode opcode, INetSerializableStruct data) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.CIRCUITBOX); + + msg.WriteNetSerializableStruct(new NetCircuitBoxHeader( + Opcode: opcode, + ItemID: item.ID, + ComponentIndex: (byte)item.GetComponentIndex(this))); + + msg.WriteNetSerializableStruct(data); + + DeliveryMethod deliveryMethod = + UnrealiableOpcodes.Contains(opcode) + ? DeliveryMethod.Unreliable + : DeliveryMethod.Reliable; + + GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod); + } + + private void SendCursorState(Vector2[] cursorPositions, Option dragStart, Option heldComponent) + { + if (!IsRoundRunning()) { return; } + + var msg = new NetCircuitBoxCursorInfo( + RecordedPositions: cursorPositions, + DragStart: dragStart, + HeldItem: heldComponent); + + SendMessage(CircuitBoxOpcode.Cursor, msg); + } + + public void ClientReadCursor(NetCircuitBoxCursorInfo info) + { + if (Entity.FindEntityByID(info.CharacterID) is not Character character) { return; } + + if (!ActiveCursors.ContainsKey(character)) + { + var newCursor = new CircuitBoxCursor(info); + ActiveCursors.Add(character, newCursor); + return; + } + + var activeCursor = ActiveCursors[character]; + activeCursor.UpdateInfo(info); + activeCursor.ResetTimers(); + } + + public void CreateClientEvent(INetSerializableStruct data) + => item.CreateClientEvent(this, new CircuitBoxEventData(data)); + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null) + { + if (extraData is null) { return; } + var eventData = ExtractEventData(extraData); + msg.WriteByte((byte)eventData.Opcode); + msg.WriteNetSerializableStruct(eventData.Data); + } + + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + var header = (CircuitBoxOpcode)msg.ReadByte(); + + switch (header) + { + case CircuitBoxOpcode.AddComponent: + { + var data = INetSerializableStruct.Read(msg); + AddComponentFromData(data); + break; + } + case CircuitBoxOpcode.DeleteComponent: + { + var data = INetSerializableStruct.Read(msg); + RemoveComponentInternal(data.TargetIDs); + break; + } + case CircuitBoxOpcode.MoveComponent: + { + var data = INetSerializableStruct.Read(msg); + MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + break; + } + case CircuitBoxOpcode.UpdateSelection: + { + var data = INetSerializableStruct.Read(msg); + + var nodeDict = data.ComponentIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); + var wireDict = data.WireIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); + var ioDict = data.InputOutputs.ToImmutableDictionary(static s => s.Type, static s => s.SelectedBy); + + UpdateSelections(nodeDict, wireDict, ioDict); + break; + } + case CircuitBoxOpcode.AddWire: + { + var data = INetSerializableStruct.Read(msg); + AddWireFromData(data); + break; + } + case CircuitBoxOpcode.RemoveWire: + { + var data = INetSerializableStruct.Read(msg); + RemoveWireInternal(data.TargetIDs); + break; + } + case CircuitBoxOpcode.ServerInitialize: + { + Components.Clear(); + Wires.Clear(); + + var data = INetSerializableStruct.Read(msg); + foreach (var compData in data.Components) { AddComponentFromData(compData); } + foreach (var wireData in data.Wires) { AddWireFromData(wireData); } + + foreach (var node in InputOutputNodes) + { + node.Position = node.NodeType switch + { + CircuitBoxInputOutputNode.Type.Input => data.InputPos, + CircuitBoxInputOutputNode.Type.Output => data.OutputPos, + _ => node.Position + }; + } + wasInitializedByServer = true; + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); + } + } + + public void AddComponentFromData(CircuitBoxServerCreateComponentEvent data) + { + if (ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.UsedResource) is not { } prefab) + { + throw new Exception($"No item prefab found for \"{data.UsedResource}\""); + } + + AddComponentInternalUnsafe(data.ComponentId, FindItemByID(data.BackingItemId), prefab, data.Position); + } + + public void AddWireFromData(CircuitBoxServerCreateWireEvent data) + { + var (req, wireId, possibleItemId) = data; + var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == req.SelectedWirePrefabIdentifier); + if (prefab is null) + { + throw new Exception($"No prefab found for \"{req.SelectedWirePrefabIdentifier}\""); + } + + if (!req.Start.FindConnection(this).TryUnwrap(out var start)) + { + throw new Exception($"No connection found for ({req.Start})"); + } + + if (!req.End.FindConnection(this).TryUnwrap(out var end)) + { + throw new Exception($"No connection found for ({req.Start})"); + } + + if (possibleItemId.TryUnwrap(out var backingItem)) + { + CreateWireWithItem(start, end, wireId, FindItemByID(backingItem)); + } + else + { + CreateWireWithoutItem(start, end, wireId, prefab); + } + } + + public static Item FindItemByID(ushort id) + => Entity.FindEntityByID(id) as Item ?? throw new Exception($"No item with ID {id} exists."); + + public override void AddToGUIUpdateList(int order = 0) + { + base.AddToGUIUpdateList(order); + UI?.AddToGUIUpdateList(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 35ed6a50a..8b7255a32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -21,6 +21,8 @@ namespace Barotrauma.Items.Components public float FlashTimer { get; private set; } public static Wire DraggingConnected { get; private set; } + private static float ConnectionSpriteSize => 35.0f * GUI.Scale; + public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character, out (Vector2 tooltipPos, LocalizedString text) tooltip) { @@ -33,8 +35,6 @@ namespace Barotrauma.Items.Components int x = panelRect.X, y = panelRect.Y; int width = panelRect.Width, height = panelRect.Height; - Vector2 scale = GetScale(panel.GuiFrame.RectTransform.MaxSize, panel.GuiFrame.Rect.Size); - bool mouseInRect = panelRect.Contains(PlayerInput.MousePosition); int totalWireCount = 0; @@ -70,15 +70,15 @@ namespace Barotrauma.Items.Components //two passes: first the connector, then the wires to get the wires to render in front for (int i = 0; i < 2; i++) { - Vector2 rightPos = GetRightPos(x, y, width, scale); - Vector2 leftPos = GetLeftPos(x, y, scale); + Vector2 rightPos = GetRightPos(x, y, width); + Vector2 leftPos = GetLeftPos(x, y); - Vector2 rightWirePos = new Vector2(x + width - 5 * scale.X, y + 30 * scale.Y); - Vector2 leftWirePos = new Vector2(x + 5 * scale.X, y + 30 * scale.Y); + Vector2 rightWirePos = new Vector2(x + width - 5 * GUI.Scale, y + 30 * GUI.Scale); + Vector2 leftWirePos = new Vector2(x + 5 * GUI.Scale, y + 30 * GUI.Scale); - int wireInterval = (height - (int)(20 * scale.Y)) / Math.Max(totalWireCount, 1); - int connectorIntervalLeft = GetConnectorIntervalLeft(height, scale, panel); - int connectorIntervalRight = GetConnectorIntervalRight(height, scale, panel); + int wireInterval = (height - (int)(20 * GUI.Scale)) / Math.Max(totalWireCount, 1); + int connectorIntervalLeft = GetConnectorIntervalLeft(height, panel); + int connectorIntervalRight = GetConnectorIntervalRight(height, panel); foreach (Connection c in panel.Connections) { @@ -101,47 +101,26 @@ namespace Barotrauma.Items.Components } Vector2 position = c.IsOutput ? rightPos : leftPos; - Color highlightColor = Color.Transparent; if (ConnectionPanel.ShouldDebugDrawWiring) { - if (c.IsPower) - { - highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red); - } - else - { - highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen); - highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange); - } - bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale); + DrawConnectionDebugInfo(spriteBatch, c, position, GUI.Scale, out var tooltipText); - LocalizedString toolTipText = c.GetToolTip(); - if (mouseOn) { tooltip = (position, toolTipText); } - if (!toolTipText.IsNullOrEmpty()) + if (!tooltipText.IsNullOrEmpty()) { - var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; - glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2, - scale: 45.0f / glowSprite.size.X * panel.Scale); + bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale); + if (mouseOn) + { + tooltip = (position, tooltipText); + } } } - static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color) - { - if (timeSinceCreated < 1.0f) - { - float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f; - Color targetColor = Color.Lerp(defaultColor, color, pulseAmount); - return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated); - } - return defaultColor; - } - //outputs are drawn at the right side of the panel, inputs at the left if (c.IsOutput) { if (i == 0) { - c.DrawConnection(spriteBatch, panel, rightPos, GetOutputLabelPosition(rightPos, panel, c), scale); + c.DrawConnection(spriteBatch, panel, rightPos, GetOutputLabelPosition(rightPos, panel, c)); } else { @@ -154,7 +133,7 @@ namespace Barotrauma.Items.Components { if (i == 0) { - c.DrawConnection(spriteBatch, panel, leftPos, GetInputLabelPosition(leftPos, panel, c), scale); + c.DrawConnection(spriteBatch, panel, leftPos, GetInputLabelPosition(leftPos, panel, c)); } else { @@ -224,8 +203,8 @@ namespace Barotrauma.Items.Components } - float step = (width * 0.75f) / panel.DisconnectedWires.Count(); - x = (int)(x + width / 2 - step * (panel.DisconnectedWires.Count() - 1) / 2); + float step = (width * 0.75f) / panel.DisconnectedWires.Count; + x = (int)(x + width / 2 - step * (panel.DisconnectedWires.Count - 1) / 2); foreach (Wire wire in panel.DisconnectedWires) { if (wire == DraggingConnected && mouseInRect) { continue; } @@ -243,26 +222,61 @@ namespace Barotrauma.Items.Components //stop dragging a wire item if the cursor is within any connection panel //(so we don't drop the item when dropping the wire on a connection) if (mouseInRect || (GUI.MouseOn?.UserData is ConnectionPanel && GUI.MouseOn.MouseRect.Contains(PlayerInput.MousePosition))) - { - Inventory.DraggingItems.Clear(); - } + { + Inventory.DraggingItems.Clear(); + } } - private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos, Vector2 scale) + public static void DrawConnectionDebugInfo(SpriteBatch spriteBatch, Connection c, Vector2 position, float scale, out LocalizedString tooltip) + { + Color highlightColor = Color.Transparent; + if (c.IsPower) + { + highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red); + } + else + { + highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen); + highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange); + } + + LocalizedString toolTipText = c.GetToolTip(); + if (!toolTipText.IsNullOrEmpty()) + { + var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; + glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2, + scale: 45.0f / glowSprite.size.X * scale); + } + + tooltip = toolTipText; + + static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color) + { + if (timeSinceCreated < 1.0f) + { + float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f; + Color targetColor = Color.Lerp(defaultColor, color, pulseAmount); + return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated); + } + return defaultColor; + } + } + + private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos) { string text = DisplayName.Value.ToUpperInvariant(); //nasty if (GUIStyle.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) { - Rectangle labelArea = GetLabelArea(labelPos, text, scale); + Rectangle labelArea = GetLabelArea(labelPos, text); labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUIStyle.Red : Color.SteelBlue); } GUI.DrawString(spriteBatch, labelPos + Vector2.UnitY, text, Color.Black * 0.8f, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont); - float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; + float connectorSpriteScale = ConnectionSpriteSize / connectionSprite.SourceRect.Width; connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale); @@ -270,7 +284,7 @@ namespace Barotrauma.Items.Components private void DrawWires(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 wirePosition, bool mouseIn, Wire equippedWire, float wireInterval) { - float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; + float connectorSpriteScale = ConnectionSpriteSize / connectionSprite.SourceRect.Width; foreach (var wire in wires) { @@ -285,9 +299,14 @@ namespace Barotrauma.Items.Components wirePosition.Y += wireInterval; } - if (DraggingConnected != null && Vector2.Distance(position, PlayerInput.MousePosition) < (20.0f * GUI.Scale)) + bool isMouseOn = Vector2.Distance(position, PlayerInput.MousePosition) < (20.0f * GUI.Scale); + if (isMouseOn) { connectionSpriteHighlight.Draw(spriteBatch, position, scale: connectorSpriteScale); + } + + if (DraggingConnected != null && isMouseOn) + { if (!PlayerInput.PrimaryMouseButtonHeld()) { @@ -418,7 +437,7 @@ namespace Barotrauma.Items.Components bool mouseOn = canDrag && - !(GUI.MouseOn is GUIDragHandle) && + GUI.MouseOn is not GUIDragHandle && ((PlayerInput.MousePosition.X > Math.Min(start.X, end.X) && PlayerInput.MousePosition.X < Math.Max(start.X, end.X) && MathUtils.LineToPointDistanceSquared(start, end, PlayerInput.MousePosition) < 36) || @@ -442,12 +461,12 @@ namespace Barotrauma.Items.Components } } - var wireEnd = end + Vector2.Normalize(start - end) * 30.0f * panel.Scale; + var wireEnd = end + Vector2.Normalize(start - end) * 30.0f * GUI.Scale; float dist = Vector2.Distance(start, wireEnd); - float wireWidth = 12 * panel.Scale; - float highlight = 5 * panel.Scale; + float wireWidth = 12 * GUI.Scale; + float highlight = 5 * GUI.Scale; if (mouseOn) { spriteBatch.Draw(wireVertical.Texture, new Rectangle(wireEnd.ToPoint(), new Point((int)(wireWidth + highlight), (int)dist)), wireVertical.SourceRect, @@ -488,110 +507,84 @@ namespace Barotrauma.Items.Components { Rectangle panelRect = panel.GuiFrame.Rect; int x = panelRect.X, y = panelRect.Y; - Vector2 scale = GetScale(panel.GuiFrame.RectTransform.MaxSize, panel.GuiFrame.Rect.Size); - Vector2 rightPos = GetRightPos(x, y, panelRect.Width, scale); - Vector2 leftPos = GetLeftPos(x, y, scale); - int connectorIntervalLeft = GetConnectorIntervalLeft(panelRect.Height, scale, panel); - int connectorIntervalRight = GetConnectorIntervalRight(panelRect.Height, scale, panel); + Vector2 rightPos = GetRightPos(x, y, panelRect.Width); + Vector2 leftPos = GetLeftPos(x, y); + int connectorIntervalLeft = GetConnectorIntervalLeft(panelRect.Height, panel); + int connectorIntervalRight = GetConnectorIntervalRight(panelRect.Height, panel); newRectSize = panelRect.Size; - var labelAreas = new List(); - for (int i = 0; i < 100; i++) + + //make sure the connection labels don't overlap horizontally + float rightMostInput = panelRect.Center.X; + float leftMostOutput = panelRect.Center.X; + foreach (var c in panel.Connections) { - labelAreas.Clear(); - foreach (var c in panel.Connections) + if (c.IsOutput) { - if (c.IsOutput) - { - var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.Value.ToUpperInvariant(), scale); - labelAreas.Add(labelArea); - rightPos.Y += connectorIntervalLeft; - } - else - { - var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.Value.ToUpperInvariant(), scale); - labelAreas.Add(labelArea); - leftPos.Y += connectorIntervalRight; - } + var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.Value.ToUpperInvariant()); + leftMostOutput = Math.Min(leftMostOutput, labelArea.X); + rightPos.Y += connectorIntervalLeft; } - bool foundOverlap = false; - for (int j = 0; j < labelAreas.Count; j++) + else { - for (int k = 0; k < labelAreas.Count; k++) - { - if (k == j) { continue; } - if (!labelAreas[j].Intersects(labelAreas[k])) { continue; } - newRectSize += new Point(10); - Point maxSize = new Point( - Math.Max(panel.GuiFrame.RectTransform.MaxSize.X, newRectSize.X), - Math.Max(panel.GuiFrame.RectTransform.MaxSize.Y, newRectSize.Y)); - scale = GetScale(maxSize, newRectSize); - rightPos = GetRightPos(x, y, newRectSize.X, scale); - leftPos = GetLeftPos(x, y, scale); - connectorIntervalLeft = GetConnectorIntervalLeft(newRectSize.Y, scale, panel); - connectorIntervalRight = GetConnectorIntervalRight(newRectSize.Y, scale, panel); - foundOverlap = true; - break; - } + var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.Value.ToUpperInvariant()); + rightMostInput = Math.Max(rightMostInput, labelArea.Right); + leftPos.Y += connectorIntervalRight; } - if (!foundOverlap) { break; } + } + if (leftMostOutput < rightMostInput) + { + newRectSize += new Point((int)(rightMostInput - leftMostOutput) + GUI.IntScale(15), 0); + } + + //make sure connection sprites don't overlap vertically + while (GetConnectorIntervalLeft(newRectSize.Y, panel) < ConnectionSpriteSize || + GetConnectorIntervalRight(newRectSize.Y, panel) < ConnectionSpriteSize) + { + newRectSize.Y += 10; } return newRectSize.X != panel.GuiFrame.Rect.Width || newRectSize.Y > panel.GuiFrame.Rect.Height; } - private static Vector2 GetScale(Point maxSize, Point size) - { - Vector2 scale = new Vector2(GUI.Scale); - if (maxSize.X < int.MaxValue) - { - scale.X = maxSize.X / size.X; - } - if (maxSize.Y < int.MaxValue) - { - scale.Y = maxSize.Y / size.Y; - } - return scale; - } - private static Vector2 GetInputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) { return new Vector2( - connectorPosition.X + 25 * panel.Scale, - connectorPosition.Y - 5 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); + connectorPosition.X + 25 * GUI.Scale, + connectorPosition.Y - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y / 2); } private static Vector2 GetOutputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) { return new Vector2( - connectorPosition.X - 25 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, - connectorPosition.Y + 5 * panel.Scale); + connectorPosition.X - 25 * GUI.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, + connectorPosition.Y - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y / 2); } - private static Rectangle GetLabelArea(Vector2 labelPos, string text, Vector2 scale) + private static Rectangle GetLabelArea(Vector2 labelPos, string text) { Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Rectangle labelArea = new Rectangle(labelPos.ToPoint(), textSize.ToPoint()); - labelArea.Inflate(10 * scale.X, 3 * scale.Y); + labelArea.Inflate(GUI.IntScale(10), GUI.IntScale(3)); return labelArea; } - private static Vector2 GetLeftPos(int x, int y, Vector2 scale) + private static Vector2 GetLeftPos(int x, int y) { - return new Vector2(x + 80 * scale.X, y + 60 * scale.Y); + return new Vector2(x + 80 * GUI.Scale, y + 60 * GUI.Scale); } - private static Vector2 GetRightPos(int x, int y, int width, Vector2 scale) + private static Vector2 GetRightPos(int x, int y, int width) { - return new Vector2(x + width - 80 * scale.X, y + 60 * scale.Y); + return new Vector2(x + width - 80 * GUI.Scale, y + 60 * GUI.Scale); } - private static int GetConnectorIntervalLeft(int height, Vector2 scale, ConnectionPanel panel) + private static int GetConnectorIntervalLeft(int height, ConnectionPanel panel) { - return (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => c.IsOutput), 1); + return (height - GUI.IntScale(60)) / Math.Max(panel.Connections.Count(c => c.IsOutput), 1); } - private static int GetConnectorIntervalRight(int height, Vector2 scale, ConnectionPanel panel) + private static int GetConnectorIntervalRight(int height, ConnectionPanel panel) { - return (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => !c.IsOutput), 1); + return (height - GUI.IntScale(60)) / Math.Max(panel.Connections.Count(c => !c.IsOutput), 1); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index d0c3dfda4..50ee85985 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -22,11 +22,6 @@ namespace Barotrauma.Items.Components private SoundChannel rewireSoundChannel; private float rewireSoundTimer; - public float Scale - { - get { return GuiFrame.Rect.Width / 400.0f; } - } - private Point originalMaxSize; private Vector2 originalRelativeSize; @@ -104,10 +99,10 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { - return character == Character.Controlled && character == user && character.SelectedItem == item; + return character == Character.Controlled && character == user && (character.SelectedItem == item || character.SelectedSecondaryItem == item); } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (character != Character.Controlled || character != user || character.SelectedItem != item) { return; } @@ -162,10 +157,11 @@ namespace Barotrauma.Items.Components //because some of the wires connected to the panel may not exist yet long msgStartPos = msg.BitPosition; msg.ReadUInt16(); //user ID - foreach (Connection _ in Connections) + byte connectionCount = msg.ReadByte(); + for (int i = 0; i < connectionCount; i++) { uint wireCount = msg.ReadVariableUInt32(); - for (int i = 0; i < wireCount; i++) + for (int j = 0; j < wireCount; j++) { msg.ReadUInt16(); } @@ -208,21 +204,25 @@ namespace Barotrauma.Items.Components connection.ClearConnections(); } - foreach (Connection connection in Connections) + byte connectionCount = msg.ReadByte(); + for (int i = 0; i < connectionCount; i++) { HashSet newWires = new HashSet(); uint wireCount = msg.ReadVariableUInt32(); - for (int i = 0; i < wireCount; i++) + for (int j = 0; j < wireCount; j++) { ushort wireId = msg.ReadUInt16(); - - if (!(Entity.FindEntityByID(wireId) is Item wireItem)) { continue; } + if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; } Wire wireComponent = wireItem.GetComponent(); if (wireComponent == null) { continue; } newWires.Add(wireComponent); } + //this may happen if the item has been deleted server-side at the point the server is writing this event to the client + if (i >= Connections.Count) { continue; } + + var connection = Connections[i]; Wire[] oldWires = connection.Wires.Where(w => !newWires.Contains(w)).ToArray(); foreach (var wire in oldWires) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 2f4c6a63c..c821da993 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -244,7 +244,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { bool elementVisibilityChanged = false; int visibleElementCount = 0; @@ -335,17 +335,18 @@ namespace Barotrauma.Items.Components if (signals == null) { return; } for (int i = 0; i < signals.Length && i < uiElements.Count; i++) { + string signal = customInterfaceElementList[i].Signal; if (uiElements[i] is GUITextBox tb) { tb.Text = Screen.Selected is { IsEditor: true } ? - customInterfaceElementList[i].Signal : - TextManager.Get(customInterfaceElementList[i].Signal).Value; + signal : + TextManager.Get(signal).Fallback(signal).Value; } else if (uiElements[i] is GUINumberInput ni) { if (ni.InputType == NumberType.Int) { - int.TryParse(customInterfaceElementList[i].Signal, out int value); + int.TryParse(signal, out int value); ni.IntValue = value; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs index 8d7ec8a22..88dde557b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components get { return new Vector2(rangeX, rangeY) * 2.0f; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index c879cb159..21cad1ace 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -86,15 +86,15 @@ namespace Barotrauma.Items.Components } OutputValue = input; - ShowOnDisplay(input, addToHistory: true, TextColor); + ShowOnDisplay(input, addToHistory: true, TextColor, isWelcomeMessage: false); item.SendSignal(input, "signal_out"); } - partial void ShowOnDisplay(string input, bool addToHistory, Color color) + partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage) { if (addToHistory) { - messageHistory.Add(new TerminalMessage(input, color)); + messageHistory.Add(new TerminalMessage(input, color, isWelcomeMessage)); while (messageHistory.Count > MaxMessages) { messageHistory.RemoveAt(0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs index 64793fa05..e0e8ebd4a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs @@ -11,9 +11,9 @@ namespace Barotrauma.Items.Components get { return new Vector2(range * 2); } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - if (!editing || !MapEntity.SelectedList.Contains(item)) return; + if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } Vector2 pos = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y); ShapeExtensions.DrawLine(spriteBatch, pos + Vector2.UnitY * range, pos - Vector2.UnitY * range, Color.Cyan * 0.5f, 2); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 81d3fa3ee..9bafa3d1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -186,12 +186,12 @@ namespace Barotrauma.Items.Components return Color.LightBlue; } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - Draw(spriteBatch, editing, Vector2.Zero, itemDepth); + Draw(spriteBatch, editing, Vector2.Zero, itemDepth, overrideColor); } - public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float itemDepth = -1, Color? overrideColor = null) { if (sections.Count == 0 && !IsActive || Hidden) { @@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components foreach (WireSection section in sections) { - section.Draw(spriteBatch, wireSprite, item.Color, drawOffset, depth, Width); + section.Draw(spriteBatch, wireSprite, overrideColor ?? item.Color, drawOffset, depth, Width); } if (nodes.Count > 0) @@ -271,13 +271,13 @@ namespace Barotrauma.Items.Components spriteBatch, wireSprite, nodes[^1] + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, - item.Color, 0.0f, Width); + overrideColor ?? item.Color, 0.0f, Width); WireSection.Draw( spriteBatch, wireSprite, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, - item.Color, itemDepth, Width); + overrideColor ?? item.Color, itemDepth, Width); GUI.DrawRectangle(spriteBatch, new Vector2(newNodePos.X + drawOffset.X, -(newNodePos.Y + drawOffset.Y)) - Vector2.One * 3, Vector2.One * 6, item.Color); } @@ -287,7 +287,7 @@ namespace Barotrauma.Items.Components spriteBatch, wireSprite, nodes[^1] + drawOffset, item.DrawPosition, - item.Color, 0.0f, Width); + overrideColor ?? item.Color, 0.0f, Width); } } } @@ -594,7 +594,7 @@ namespace Barotrauma.Items.Components selectedWire.shouldClearConnections = false; Character.Controlled.Inventory.TryPutItem(selectedWire.item, Character.Controlled, new List { InvSlotType.LeftHand, InvSlotType.RightHand }); - foreach (var entity in MapEntity.mapEntityList) + foreach (var entity in MapEntity.MapEntityList) { if (entity is Item item) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 402805f70..5e5cc982f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -117,6 +117,13 @@ namespace Barotrauma.Items.Components get { return barrelSprite; } } + [Serialize(false, IsPropertySaveable.No)] + public bool HideBarrelWhenBroken + { + get; + private set; + } + partial void InitProjSpecific(ContentXElement element) { foreach (var subElement in element.Elements()) @@ -232,7 +239,7 @@ namespace Barotrauma.Items.Components { moveSoundChannel.FadeOutAndDispose(); moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling, freqMult: endMoveSound.GetRandomFrequencyMultiplier()); - if (moveSoundChannel != null) moveSoundChannel.Looping = false; + if (moveSoundChannel != null) { moveSoundChannel.Looping = false; } } else if (!moveSoundChannel.IsPlaying) { @@ -261,12 +268,13 @@ namespace Barotrauma.Items.Components if (chargeSound != null) { chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier()); - if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; } } } else if (chargeSoundChannel != null) { chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; default: @@ -318,7 +326,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (crosshairSprite != null) { @@ -359,7 +367,7 @@ namespace Barotrauma.Items.Components return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) { @@ -367,53 +375,55 @@ namespace Barotrauma.Items.Components } Vector2 drawPos = GetDrawPos(); - - railSprite?.Draw(spriteBatch, - drawPos, - item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); - - barrelSprite?.Draw(spriteBatch, - drawPos - GetRecoilOffset() * item.Scale, - item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); - - float chargeRatio = currentChargeTime / MaxChargeTime; - - foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) + if (item.Condition > 0.0f || !HideBarrelWhenBroken) { - chargeSprite?.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), - item.SpriteColor, + railSprite?.Draw(spriteBatch, + drawPos, + overrideColor ?? item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); - } + SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); - int spinningBarrelCount = spinningBarrelSprites.Count; - - for (int i = 0; i < spinningBarrelCount; i++) - { - // this block is messy since I was debugging it with a bunch of values, should be cleaned up / optimized if prototype is accepted - Sprite spinningBarrel = spinningBarrelSprites[i]; - float barrelCirclePosition = (MaxCircle * i / spinningBarrelCount + currentBarrelSpin) % MaxCircle; - - float newDepth = item.SpriteDepth + (spinningBarrel.Depth - item.Sprite.Depth) + (barrelCirclePosition > HalfCircle ? 0.0f : 0.001f); - - float barrelColorPosition = (barrelCirclePosition + QuarterCircle) % MaxCircle; - float colorOffset = Math.Abs(barrelColorPosition - HalfCircle) / HalfCircle; - Color newColorModifier = Color.Lerp(Color.Black, Color.Gray, colorOffset); - - float barrelHalfCirclePosition = Math.Abs(barrelCirclePosition - HalfCircle); - float barrelPositionModifier = MathUtils.SmoothStep(barrelHalfCirclePosition / HalfCircle); - float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; - - spinningBarrel.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2), - Color.Lerp(item.SpriteColor, newColorModifier, 0.8f), + barrelSprite?.Draw(spriteBatch, + drawPos - GetRecoilOffset() * item.Scale, + overrideColor ?? item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, newDepth); + SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); + + float chargeRatio = currentChargeTime / MaxChargeTime; + + foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) + { + chargeSprite?.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), + item.SpriteColor, + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); + } + + int spinningBarrelCount = spinningBarrelSprites.Count; + + for (int i = 0; i < spinningBarrelCount; i++) + { + // this block is messy since I was debugging it with a bunch of values, should be cleaned up / optimized if prototype is accepted + Sprite spinningBarrel = spinningBarrelSprites[i]; + float barrelCirclePosition = (MaxCircle * i / spinningBarrelCount + currentBarrelSpin) % MaxCircle; + + float newDepth = item.SpriteDepth + (spinningBarrel.Depth - item.Sprite.Depth) + (barrelCirclePosition > HalfCircle ? 0.0f : 0.001f); + + float barrelColorPosition = (barrelCirclePosition + QuarterCircle) % MaxCircle; + float colorOffset = Math.Abs(barrelColorPosition - HalfCircle) / HalfCircle; + Color newColorModifier = Color.Lerp(Color.Black, Color.Gray, colorOffset); + + float barrelHalfCirclePosition = Math.Abs(barrelCirclePosition - HalfCircle); + float barrelPositionModifier = MathUtils.SmoothStep(barrelHalfCirclePosition / HalfCircle); + float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; + + spinningBarrel.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2), + Color.Lerp(overrideColor ?? item.SpriteColor, newColorModifier, 0.8f), + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, newDepth); + } } if (GameMain.DebugDraw) @@ -593,6 +603,7 @@ namespace Barotrauma.Items.Components if (!recipient.IsPower || !recipient.IsOutput) { continue; } var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } + if (battery.OutputDisabled) { continue; } availableCharge += battery.Charge; availableCapacity += battery.GetCapacity(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index e1f12a51b..55a920129 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (dockingState == 0.0f) return; @@ -39,7 +39,8 @@ namespace Barotrauma.Items.Components drawPos, new Rectangle( rect.Center.X + (int)(rect.Width / 2 * (1.0f - dockingState)), rect.Y, - (int)(rect.Width / 2 * dockingState), rect.Height), Color.White); + (int)(rect.Width / 2 * dockingState), rect.Height), + overrideColor ?? Color.White); } else @@ -48,7 +49,8 @@ namespace Barotrauma.Items.Components drawPos - Vector2.UnitX * (rect.Width / 2 * dockingState), new Rectangle( rect.X, rect.Y, - (int)(rect.Width / 2 * dockingState), rect.Height), Color.White); + (int)(rect.Width / 2 * dockingState), rect.Height), + overrideColor ?? Color.White); } } else @@ -61,7 +63,8 @@ namespace Barotrauma.Items.Components drawPos - Vector2.UnitY * (rect.Height / 2 * dockingState), new Rectangle( rect.X, rect.Y, - rect.Width, (int)(rect.Height / 2 * dockingState)), Color.White); + rect.Width, (int)(rect.Height / 2 * dockingState)), + overrideColor ?? Color.White); } else { @@ -69,7 +72,8 @@ namespace Barotrauma.Items.Components drawPos, new Rectangle( rect.X, rect.Y + rect.Height / 2 + (int)(rect.Height / 2 * (1.0f - dockingState)), - rect.Width, (int)(rect.Height / 2 * dockingState)), Color.White); + rect.Width, (int)(rect.Height / 2 * dockingState)), + overrideColor ?? Color.White); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 96607abbd..ac8f2e935 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -182,6 +182,7 @@ namespace Barotrauma public Rectangle BackgroundFrame { get; protected set; } + private List[] partialReceivedItemIDs; private List[] receivedItemIDs; private CoroutineHandle syncItemsCoroutine; @@ -257,7 +258,7 @@ namespace Barotrauma else { LocalizedString description = item.Description; - if (item.HasTag("identitycard") || item.HasTag("despawncontainer")) + if (item.HasTag(Tags.IdCard) || item.HasTag(Tags.DespawnContainer)) { string[] readTags = item.Tags.Split(','); string idName = null; @@ -347,6 +348,9 @@ namespace Barotrauma { toolTip += item.Prefab.GetSkillRequirementHints(character); } +#if DEBUG + toolTip += $" ({item.Prefab.Identifier})"; +#endif return RichString.Rich(toolTip); } } @@ -387,6 +391,12 @@ namespace Barotrauma /// public RectTransform RectTransform; + /// + /// Normally false - we don't draw the UI because it's drawn when the player hovers the cursor over the item in their inventory. + /// Enabled in special cases like equippable fabricators where the inventory is a part of the fabricator UI. + /// + public bool DrawWhenEquipped; + public static SlotReference SelectedSlot { get @@ -417,6 +427,7 @@ namespace Barotrauma padding = new Vector4(spacing.X, spacing.Y, spacing.X, spacing.X); + Vector2 slotAreaSize = new Vector2( columns * rectSize.X + (columns - 1) * spacing.X, rows * rectSize.Y + (rows - 1) * spacing.Y); @@ -427,6 +438,8 @@ namespace Barotrauma GameMain.GraphicsWidth / 2 - slotAreaSize.X / 2, GameMain.GraphicsHeight / 2 - slotAreaSize.Y / 2); + Vector2 center = topLeft + slotAreaSize / 2; + if (RectTransform != null) { Vector2 scale = new Vector2( @@ -438,6 +451,8 @@ namespace Barotrauma padding.X *= scale.X; padding.Z *= scale.X; padding.Y *= scale.Y; padding.W *= scale.Y; + center = RectTransform.Rect.Center.ToVector2(); + topLeft = RectTransform.TopLeft.ToVector2() + new Vector2(padding.X, padding.Y); prevRect = RectTransform.Rect; } @@ -445,8 +460,14 @@ namespace Barotrauma Rectangle slotRect = new Rectangle((int)topLeft.X, (int)topLeft.Y, (int)rectSize.X, (int)rectSize.Y); for (int i = 0; i < capacity; i++) { - slotRect.X = (int)(topLeft.X + (rectSize.X + spacing.X) * (i % slotsPerRow)); - slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * ((int)Math.Floor((double)i / slotsPerRow))); + int row = (int)Math.Floor((double)i / slotsPerRow); + int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow); + + int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1)); + slotRect.X = (int)(center.X) - rowWidth / 2; + slotRect.X += (int)((rectSize.X + spacing.X) * (i % slotsPerThisRow)); + + slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * row); visualSlots[i] = new VisualSlot(slotRect); visualSlots[i].InteractRect = new Rectangle( (int)(visualSlots[i].Rect.X - spacing.X / 2 - 1), (int)(visualSlots[i].Rect.Y - spacing.Y / 2 - 1), @@ -489,7 +510,7 @@ namespace Barotrauma return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); } - protected virtual bool HideSlot(int i) + public virtual bool HideSlot(int i) { return visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty()); } @@ -673,6 +694,7 @@ namespace Barotrauma var container = item.GetComponent(); if (container == null || !container.DrawInventory) { return; } + if (container.Inventory.DrawWhenEquipped) { return; } var subInventory = container.Inventory; if (subInventory.visualSlots == null) { subInventory.CreateSlots(); } @@ -871,44 +893,34 @@ namespace Barotrauma if (Character.Controlled.Inventory != null && !isSubEditor) { - var inv = Character.Controlled.Inventory; - for (var i = 0; i < inv.visualSlots.Length; i++) + if (IsOnInventorySlot(Character.Controlled.Inventory)) { return true; } + } + + if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) + { + if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; } + } + + bool IsOnInventorySlot(Inventory inventory) + { + for (var i = 0; i < inventory.visualSlots.Length; i++) { - var slot = inv.visualSlots[i]; + if (inventory.HideSlot(i)) { continue; } + var slot = inventory.visualSlots[i]; if (slot.InteractRect.Contains(PlayerInput.MousePosition)) { return true; } // check if the equip button actually exists - if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && - i >= 0 && inv.slots.Length > i && - !inv.slots[i].Empty()) - { - return true; - } - } - } - - if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) - { - var inv = Character.Controlled.SelectedCharacter.Inventory; - for (var i = 0; i < inv.visualSlots.Length; i++) - { - var slot = inv.visualSlots[i]; - if (slot.InteractRect.Contains(PlayerInput.MousePosition)) - { - return true; - } - - // check if the equip button actually exists - if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && - i >= 0 && inv.slots.Length > i && - !inv.slots[i].Empty()) + if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && + i >= 0 && inventory.slots.Length > i && + !inventory.slots[i].Empty()) { return true; } } + return false; } if (Character.Controlled.SelectedItem != null) @@ -1034,7 +1046,7 @@ namespace Barotrauma protected static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle highlightedSlot) { - GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot); + GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, Anchor.BottomRight); } public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) @@ -1046,6 +1058,7 @@ namespace Barotrauma if (container == null || !container.DrawInventory) { return; } if (container.Inventory.visualSlots == null || !container.Inventory.isSubInventory) { return; } + if (container.Inventory.DrawWhenEquipped) { return; } int itemCapacity = container.Capacity; @@ -1215,8 +1228,8 @@ namespace Barotrauma else { DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); + DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false); } - SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } } @@ -1258,11 +1271,19 @@ namespace Barotrauma { allowCombine = false; } + int itemCount = 0; foreach (Item item in DraggingItems) { bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); - anySuccess |= success; - if (!success) { break; } + if (success) + { + anySuccess = true; + itemCount++; + } + if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory)) + { + break; + } } if (anySuccess) @@ -1360,10 +1381,12 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { Rectangle hoverArea; - if (!subSlot.Inventory.Movable() || + 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)) + (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default))) { + //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); @@ -1372,7 +1395,10 @@ namespace Barotrauma { hoverArea = subSlot.Inventory.BackgroundFrame; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); - hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); + if (subSlot.Inventory.movableFrameRect != Rectangle.Empty) + { + hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); + } } if (subSlot.Inventory?.visualSlots != null) @@ -1465,18 +1491,28 @@ namespace Barotrauma color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen, font: GUIStyle.SmallFont); } + + Item draggedItem = DraggingItems.First(); + sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black, scale: scale); sprite.Draw(spriteBatch, itemPos, - sprite == DraggingItems.First().Sprite ? DraggingItems.First().GetSpriteColor() : DraggingItems.First().GetInventoryIconColor(), + sprite == draggedItem.Sprite ? draggedItem.GetSpriteColor() : draggedItem.GetInventoryIconColor(), scale: scale); - if (DraggingItems.First().Prefab.MaxStackSize > 1) + if (draggedItem.Prefab.GetMaxStackSize(null) > 1) { + int stackAmount = DraggingItems.Count; + if (selectedSlot?.ParentInventory != null) + { + stackAmount = Math.Min( + stackAmount, + selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition)); + } Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; - string stackCountText = "x" + DraggingItems.Count; + string stackCountText = "x" + stackAmount; GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright); } } } @@ -1673,7 +1709,7 @@ namespace Barotrauma color: GUIStyle.Red, scale: iconSize.X / stealIcon.size.X); } - int maxStackSize = item.Prefab.MaxStackSize; + int maxStackSize = item.Prefab.GetMaxStackSize(inventory); if (inventory is ItemInventory itemInventory) { maxStackSize = Math.Min(maxStackSize, itemInventory.Container.GetMaxStackSize(slotIndex)); @@ -1691,7 +1727,7 @@ namespace Barotrauma } } - if (HealingCooldown.IsOnCooldown && item.HasTag(HealingCooldown.MedicalItemTag)) + if (HealingCooldown.IsOnCooldown && item.HasTag(Tags.MedicalItem)) { RectangleF cdRect = rect; // shrink the rect from top to bottom depending on HealingCooldown.NormalizedCooldown @@ -1794,10 +1830,15 @@ namespace Barotrauma } } - public void ClientEventRead(IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg) { UInt16 lastEventID = msg.ReadUInt16(); - SharedRead(msg, out receivedItemIDs); + partialReceivedItemIDs ??= new List[capacity]; + SharedRead(msg, partialReceivedItemIDs, out bool readyToApply); + if (!readyToApply) { return; } + + receivedItemIDs = partialReceivedItemIDs.ToArray(); + partialReceivedItemIDs = null; //delay applying the new state if less than 1 second has passed since this client last sent a state to the server //prevents the inventory from briefly reverting to an old state if items are moved around in quick succession @@ -1866,7 +1907,7 @@ namespace Barotrauma if (!receivedItemIDs[i].Any()) { continue; } foreach (UInt16 id in receivedItemIDs[i]) { - if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } + if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } if (!TryPutItem(item, i, false, false, null, false)) { ForceToSlot(item, i); @@ -1883,11 +1924,5 @@ namespace Barotrauma receivedItemIDs = null; } - - public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) - { - SharedWrite(msg, extraData); - syncItemsDelay = 1.0f; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index c7da76748..0f42a313c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -38,7 +38,7 @@ namespace Barotrauma } } - partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients) { if (interactionType == CampaignMode.InteractionType.None) { @@ -316,6 +316,11 @@ namespace Barotrauma } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) + { + Draw(spriteBatch, editing, back, overrideColor: null); + } + + public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null) { if (!Visible || (!editing && HiddenInGame) || !SubEditorScreen.IsLayerVisible(this)) { return; } @@ -328,7 +333,9 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true); + Color color = + overrideColor ?? + (IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true)); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; @@ -412,15 +419,7 @@ namespace Barotrauma } else { - Vector2 origin = activeSprite.Origin; - if ((activeSprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) - { - origin.X = activeSprite.SourceRect.Width - origin.X; - } - if ((activeSprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) - { - origin.Y = activeSprite.SourceRect.Height - origin.Y; - } + Vector2 origin = GetSpriteOrigin(activeSprite); if (color.A > 0) { activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); @@ -484,7 +483,8 @@ namespace Barotrauma } } } - body.Draw(spriteBatch, activeSprite, color, depth, Scale); + Vector2 origin = GetSpriteOrigin(activeSprite); + body.Draw(spriteBatch, activeSprite, color, depth, Scale, origin: origin); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); @@ -536,7 +536,7 @@ namespace Barotrauma //causing them to be removed from the list for (int i = drawableComponents.Count - 1; i >= 0; i--) { - drawableComponents[i].Draw(spriteBatch, editing, depth); + drawableComponents[i].Draw(spriteBatch, editing, depth, overrideColor); } if (GameMain.DebugDraw) @@ -604,6 +604,20 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, from, to, lineColor, width: 1); //GUI.DrawString(spriteBatch, from, $"Linked to {e.Name}", lineColor, Color.Black * 0.5f); } + + Vector2 GetSpriteOrigin(Sprite sprite) + { + Vector2 origin = sprite.Origin; + if ((sprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) + { + origin.X = sprite.SourceRect.Width - origin.X; + } + if ((sprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) + { + origin.Y = sprite.SourceRect.Height - origin.Y; + } + return origin; + } } partial void OnCollisionProjSpecific(float impact) @@ -1076,7 +1090,7 @@ namespace Barotrauma if (!ignoreLocking && ic.LockGuiFramePosition) { continue; } //if the frame covers nearly all of the screen, don't trying to prevent overlaps because it'd fail anyway if (ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f) { continue; } - ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; + ic.GuiFrame.RectTransform.ScreenSpaceOffset = ic.GuiFrameOffset; elementsToMove.Add(ic.GuiFrame); debugInitialHudPositions.Add(ic.GuiFrame.Rect); } @@ -1281,6 +1295,11 @@ namespace Barotrauma nameText += $" ({idName})"; } } + if (DroppedStack.Any()) + { + nameText += $" x{DroppedStack.Count()}"; + } + texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, false, false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) @@ -1350,10 +1369,16 @@ namespace Barotrauma } } - if (Character.Controlled != null && Character.Controlled.SelectedItem != this && GetComponent() == null) + var character = Character.Controlled; + var selectedItem = Character.Controlled?.SelectedItem; + if (character != null && selectedItem != this && GetComponent() == null) { - if (Character.Controlled.SelectedItem?.GetComponent()?.TargetItem != this && - !Character.Controlled.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) + bool insideCircuitBox = + selectedItem?.GetComponent() != null && + selectedItem.ContainedItems.Contains(this); + if (!insideCircuitBox && + selectedItem?.GetComponent()?.TargetItem != this && + !character.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) { return; } @@ -1407,7 +1432,7 @@ namespace Barotrauma int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[containerIndex] is ItemContainer container) { - container.Inventory.ClientEventRead(msg, sendingTime); + container.Inventory.ClientEventRead(msg); } else { @@ -1421,7 +1446,12 @@ namespace Barotrauma SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound); break; case EventType.AssignCampaignInteraction: - CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); + bool isVisible = msg.ReadBoolean(); + if (isVisible) + { + var interactionType = (CampaignMode.InteractionType)msg.ReadByte(); + AssignCampaignInteractionType(interactionType); + } break; case EventType.ApplyStatusEffect: { @@ -1486,6 +1516,30 @@ namespace Barotrauma AddUpgrade(upgrade, false); } break; + case EventType.DroppedStack: + int itemCount = msg.ReadRangedInteger(0, Inventory.MaxPossibleStackSize); + if (itemCount > 0) + { + List droppedStack = new List(); + for (int i = 0; i < itemCount; i++) + { + var id = msg.ReadUInt16(); + if (FindEntityByID(id) is not Item droppedItem) + { + DebugConsole.ThrowError($"Error while reading {EventType.DroppedStack} message: could not find an item with the ID {id}."); + } + else + { + droppedStack.Add(droppedItem); + } + } + CreateDroppedStack(droppedStack, allowClientExecute: true); + } + else + { + RemoveFromDroppedStack(allowClientExecute: true); + } + break; default: throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } @@ -1511,7 +1565,7 @@ namespace Barotrauma { var component = componentStateEventData.Component; if (component is null) { throw error("component was null"); } - if (!(component is IClientSerializable clientSerializable)) { throw error($"component was not {nameof(IClientSerializable)}"); } + if (component is not IClientSerializable clientSerializable) { throw error($"component was not {nameof(IClientSerializable)}"); } int componentIndex = components.IndexOf(component); if (componentIndex < 0) { throw error("component did not belong to item"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); @@ -1525,7 +1579,7 @@ namespace Barotrauma int containerIndex = components.IndexOf(container); if (containerIndex < 0) { throw error("container did not belong to item"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); - container.Inventory.ClientEventWrite(msg, extraData); + container.Inventory.ClientEventWrite(msg, inventoryStateEventData); } break; case TreatmentEventData treatmentEventData: @@ -1551,7 +1605,7 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } - if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent()?.IsStuckToTarget ?? false)) + if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent() is { IsStuckToTarget: true })) { positionBuffer.Clear(); return; @@ -1567,12 +1621,20 @@ namespace Barotrauma body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity); body.LinearVelocity = newVelocity; body.AngularVelocity = newAngularVelocity; - if (Vector2.DistanceSquared(newPosition, body.SimPosition) > 0.0001f || + float distSqr = Vector2.DistanceSquared(newPosition, body.SimPosition); + + if (distSqr > 0.0001f || Math.Abs(newRotation - body.Rotation) > 0.01f) { body.TargetPosition = newPosition; body.TargetRotation = newRotation; body.MoveToTargetPosition(lerp: true); + if (distSqr > 10.0f * 10.0f) + { + //very large change in position, we need to recheck which submarine the item is in + Submarine = null; + UpdateTransform(); + } } Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); @@ -1594,8 +1656,12 @@ namespace Barotrauma return; } + var posInfo = body.ClientRead(msg, sendingTime, parentDebugName: Name); msg.ReadPadBits(); + + if (GetComponent() is { IsStuckToTarget: true }) { return; } + if (posInfo != null) { int index = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs index 35e6f8385..97cff0b73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -83,5 +84,11 @@ namespace Barotrauma base.Draw(spriteBatch, subInventory); } } + + public void ClientEventWrite(IWriteMessage msg, Item.InventoryStateEventData extraData) + { + SharedWrite(msg, extraData.SlotRange); + syncItemsDelay = 1.0f; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index ad94444ce..b422225e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -1,5 +1,7 @@ using Barotrauma.Lights; +using Barotrauma.Particles; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; namespace Barotrauma @@ -28,36 +30,60 @@ namespace Barotrauma underwaterExplosion.StartDelay = 0.0f; } } - - for (int i = 0; i < Attack.Range * 0.1f; i++) + if (!underwater && (flames || smoke)) { - if (!underwater) + for (int i = 0; i < Attack.Range * 0.025f; i++) { - float particleSpeed = Rand.Range(0.0f, 1.0f); - particleSpeed = particleSpeed * particleSpeed * Attack.Range; + float distFactor = 0.0f; + + if (i > 0 && Attack.Range > 100.0f) + { + distFactor = Rand.Range(0.0f, 1.0f); + //sqrt to make larger values more common (= more particles spawn further away from the origin) + distFactor = MathF.Sqrt(distFactor); + } + float sizeFactor = MathHelper.Clamp(Attack.Range / 1000.0f, 0.0f, 1.0f); + float minScale = MathHelper.Lerp(0.2f, 1.0f, sizeFactor); + float maxScale = MathUtils.InverseLerp(2.0f, 3.0f, sizeFactor); + //larger particles closer to the origin + float particleScale = MathHelper.Clamp(1.0f - distFactor, minScale, maxScale); + + var particlePrefab = ParticleManager.FindPrefab("explosionfire"); + Vector2 pos = worldPosition; + if (i > 0) + { + pos = ClampParticlePos(worldPosition + Rand.Vector(Attack.Range * distFactor * 0.3f), hull, particlePrefab); + } if (flames) { - float particleScale = MathHelper.Clamp(Attack.Range * 0.0025f, 0.5f, 2.0f); - var flameParticle = GameMain.ParticleManager.CreateParticle("explosionfire", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), - Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); - if (flameParticle != null) flameParticle.Size *= particleScale; + var flameParticle = GameMain.ParticleManager.CreateParticle(particlePrefab, + pos, + velocity: Vector2.Zero, hullGuess: hull); + if (flameParticle != null) + { + //brief delay to particles futher from origin + flameParticle.StartDelay = distFactor * sizeFactor; + flameParticle.Size *= particleScale; + } } if (smoke) { - GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), - Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); + GameMain.ParticleManager.CreateParticle( + ParticleManager.FindPrefab(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke"), + pos, velocity: Vector2.Zero, hullGuess: hull); } } - else if (underwaterBubble) + } + + for (int i = 0; i < Attack.Range * 0.1f; i++) + { + if (underwater && underwaterBubble) { Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, Attack.Range * 0.5f)); GameMain.ParticleManager.CreateParticle("risingbubbles", worldPosition + bubblePos, - Vector2.Zero, 0.0f, hull); - + velocity: Vector2.Zero, hullGuess: hull); if (i < Attack.Range * 0.02f) { var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition + bubblePos, @@ -66,33 +92,46 @@ namespace Barotrauma { underwaterExplosion.Size *= MathHelper.Clamp(Attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); } - } - + } } - if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, - Rand.Vector(Rand.Range(1200.0f, 2400.0f)), 0.0f, hull); + Rand.Vector(Rand.Range(800.0f, 1500.0f)), 0.0f, hull); + } + if (debris) + { + GameMain.ParticleManager.CreateParticle("explosiondebris", worldPosition, + Rand.Vector(Rand.Range(800.0f, 2000.0f)), 0.0f, hull); } } if (flash) { - float displayRange = flashRange ?? Attack.Range; + float displayRange = flashRange ?? (Attack.Range * 2); if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, flashColor, null); CoroutineManager.StartCoroutine(DimLight(light)); } } - private Vector2 ClampParticlePos(Vector2 particlePos, Hull hull) + private static Vector2 ClampParticlePos(Vector2 particlePos, Hull hull, ParticlePrefab particlePrefab) { - if (hull == null) return particlePos; + float minX = hull.WorldRect.X; + float maxX = hull.WorldRect.Right; + float minY = hull.WorldRect.Y - hull.WorldRect.Height; + float maxY = hull.WorldRect.Y; + if (particlePrefab != null) + { + minX = Math.Min(minX + particlePrefab.CollisionRadius, hull.WorldRect.Center.X); + maxX = Math.Max(maxX - particlePrefab.CollisionRadius, hull.WorldRect.Center.X); + minY = Math.Min(minY + particlePrefab.CollisionRadius, hull.WorldRect.Y - hull.WorldRect.Height / 2); + maxY = Math.Max(maxY - particlePrefab.CollisionRadius, hull.WorldRect.Y - hull.WorldRect.Height / 2); + } return new Vector2( - MathHelper.Clamp(particlePos.X, hull.WorldRect.X, hull.WorldRect.Right), - MathHelper.Clamp(particlePos.Y, hull.WorldRect.Y - hull.WorldRect.Height, hull.WorldRect.Y)); + MathHelper.Clamp(particlePos.X, minX, maxX), + MathHelper.Clamp(particlePos.Y, minY, maxY)); } private IEnumerable DimLight(LightSource light) @@ -100,9 +139,11 @@ namespace Barotrauma float currBrightness = 1.0f; while (light.Color.A > 0.0f && flashDuration > 0.0f && currBrightness > 0.0f) { - light.Color = new Color(light.Color.R, light.Color.G, light.Color.B, (byte)(currBrightness * 255)); - currBrightness -= 1.0f / flashDuration * CoroutineManager.DeltaTime; - + if (!CoroutineManager.Paused) + { + light.Color = new Color(light.Color.R, light.Color.G, light.Color.B, (byte)(currBrightness * 255)); + currBrightness -= 1.0f / flashDuration * CoroutineManager.DeltaTime; + } yield return CoroutineStatus.Running; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index d23d0b542..4ec014826 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -199,7 +199,7 @@ namespace Barotrauma Sprite activeSprite = obj.Sprite; activeSprite?.Draw( spriteBatch, - new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, + new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 3000.0f), activeSprite.Origin, obj.CurrentRotation, @@ -218,7 +218,7 @@ namespace Barotrauma obj.ActivePrefab.DeformableSprite.Reset(); } obj.ActivePrefab.DeformableSprite?.Draw(cam, - new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, z * 10.0f), + new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f), obj.ActivePrefab.DeformableSprite.Origin, obj.CurrentRotation, obj.CurrentScale, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 7faf33912..82ad83280 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -561,7 +561,7 @@ namespace Barotrauma.Lights if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos)) { - Array.Reverse(ShadowVertices); + Array.Reverse(ShadowVertices, 0, ShadowVertexCount); } CalculateLosPenumbraVertices(lightSourcePos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e846bfa4f..c04db86d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -455,6 +455,10 @@ namespace Barotrauma.Lights if (drawDeformSprites == (limb.DeformSprite == null)) { continue; } limb.Draw(spriteBatch, cam, lightColor); } + foreach (var heldItem in character.HeldItems) + { + heldItem.Draw(spriteBatch, editing: false, overrideColor: Color.Black); + } } } @@ -480,9 +484,9 @@ namespace Barotrauma.Lights if (ConnectionPanel.ShouldDebugDrawWiring) { - foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.mapEntityList)) + foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.MapEntityList)) { - if (e is Item item && item.GetComponent() is Wire wire) + if (e is Item item && !item.HiddenInGame && item.GetComponent() is Wire wire) { wire.DebugDraw(spriteBatch, alpha: 0.4f); } @@ -719,6 +723,7 @@ namespace Barotrauma.Lights { foreach (var ch in convexHulls) { + if (!ch.Enabled) { continue; } Vector2 currentViewPos = pos; Vector2 defaultViewPos = ViewTarget.DrawPosition; if (ch.ParentEntity?.Submarine != null) @@ -742,10 +747,13 @@ namespace Barotrauma.Lights { if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } - Vector2 relativeLightPos = pos; - if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } + Vector2 relativeViewPos = pos; + if (convexHull.ParentEntity?.Submarine != null) + { + relativeViewPos -= convexHull.ParentEntity.Submarine.DrawPosition; + } - convexHull.CalculateLosVertices(relativeLightPos); + convexHull.CalculateLosVertices(relativeViewPos); for (int i = 0; i < convexHull.ShadowVertexCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 9574a3d6a..7606b40c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -53,6 +53,10 @@ namespace Barotrauma.Lights [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ + " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] + public bool Directional { get; set; } + public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation))); private float flicker; @@ -206,6 +210,8 @@ namespace Barotrauma.Lights private readonly List convexHullsInRange; + private readonly HashSet visibleConvexHulls = new HashSet(); + public Texture2D texture; public SpriteEffects LightSpriteEffect; @@ -312,6 +318,8 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; + dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); + if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { return; @@ -322,6 +330,8 @@ namespace Barotrauma.Lights } } + private Vector2 dir = Vector2.UnitX; + private Vector2 _spriteScale = Vector2.One; public Vector2 SpriteScale @@ -445,7 +455,7 @@ namespace Barotrauma.Lights public bool Enabled = true; private readonly ISerializableEntity conditionalTarget; - private readonly PropertyConditional.Comparison comparison; + private readonly PropertyConditional.LogicalOperatorType logicalOperator; private readonly List conditionals = new List(); public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) @@ -453,11 +463,7 @@ namespace Barotrauma.Lights { lightSourceParams = new LightSourceParams(element); CastShadows = element.GetAttributeBool("castshadows", true); - string comparison = element.GetAttributeString("comparison", null); - if (comparison != null) - { - Enum.TryParse(comparison, ignoreCase: true, out this.comparison); - } + logicalOperator = element.GetAttributeEnum("comparison", logicalOperator); if (lightSourceParams.DeformableLightSpriteElement != null) { @@ -470,13 +476,7 @@ namespace Barotrauma.Lights switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } @@ -539,11 +539,32 @@ namespace Barotrauma.Lights var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } + //used to check whether the lightsource hits the target hull if the light is directional + Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange; + Vector2 normal = new Vector2(-ray.Y, ray.X); + chList.List.Clear(); foreach (var convexHull in fullChList.List) { if (!convexHull.Enabled) { continue; } if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } + if (lightSourceParams.Directional) + { + Rectangle bounds = convexHull.BoundingBox; + //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left + bounds.Y -= bounds.Height; + + //the ray can't hit if + // center is in the opposite direction from the ray (cheapest check first) + if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 && + /*ray doesn't hit the convex hull*/ + !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) && + /*normal vectors of the ray don't hit the convex hull */ + !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _)) + { + continue; + } + } chList.List.Add(convexHull); } chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); @@ -717,6 +738,8 @@ namespace Barotrauma.Lights public void RayCastTask(Vector2 drawPos, float rotation) { + visibleConvexHulls.Clear(); + Vector2 drawOffset = Vector2.Zero; float boundsExtended = TextureRange; if (OverrideLightTexture != null) @@ -854,13 +877,15 @@ namespace Barotrauma.Lights } } + const float MinPointDistance = 6; + //remove points that are very close to each other for (int i = 0; i < points.Count; i++) { for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance) { points.RemoveAt(j); } @@ -890,7 +915,7 @@ namespace Barotrauma.Lights foreach (SegmentPoint p in points) { Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); - Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; + Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance; //do two slightly offset raycasts to hit the segment itself and whatever's behind it var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); @@ -904,17 +929,12 @@ namespace Barotrauma.Lights bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; + bool markAsVisible = false; if (isPoint1 && isPoint2) { //hit at the current segmentpoint -> place the segmentpoint into the list verts.Add(p.WorldPos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } + markAsVisible = true; } else if (intersection1.index != intersection2.index) { @@ -922,13 +942,13 @@ namespace Barotrauma.Lights //we definitely want to generate new geometry here verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } + markAsVisible = true; + } + if (markAsVisible) + { + visibleConvexHulls.Add(p.ConvexHull); + visibleConvexHulls.Add(seg1.ConvexHull); + visibleConvexHulls.Add(seg2.ConvexHull); } //if neither of the conditions above are met, we just assume //that the raycasts both resulted on the same segment @@ -1029,11 +1049,9 @@ namespace Barotrauma.Lights Vector2 drawPos = calculatedDrawPos; - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); - Vector2 uvOffset = Vector2.Zero; Vector2 overrideTextureDims = Vector2.One; + Vector2 dir = this.dir; if (OverrideLightTexture != null) { overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -1042,8 +1060,7 @@ namespace Barotrauma.Lights if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; - cosAngle = -cosAngle; - sinAngle = -sinAngle; + dir = -dir; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); @@ -1116,8 +1133,8 @@ namespace Barotrauma.Lights //calculate texture coordinates based on the light's rotation Vector2 originDiff = diff; - diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle; - diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle; + diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y; + diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X; diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); diff += uvOffset; } @@ -1222,9 +1239,6 @@ namespace Barotrauma.Lights } drawPos.Y = -drawPos.Y; - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); - float bounds = TextureRange; if (OverrideLightTexture != null) @@ -1236,8 +1250,8 @@ namespace Barotrauma.Lights origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin *= TextureRange; - drawPos.X += origin.X * sinAngle + origin.Y * cosAngle; - drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle; + drawPos.X += origin.X * dir.Y + origin.Y * dir.X; + drawPos.Y += origin.X * dir.X + origin.Y * dir.Y; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1343,7 +1357,7 @@ namespace Barotrauma.Lights { if (conditionals.None()) { return; } if (conditionalTarget == null) { return; } - if (comparison == PropertyConditional.Comparison.And) + if (logicalOperator == PropertyConditional.LogicalOperatorType.And) { Enabled = conditionals.All(c => c.Matches(conditionalTarget)); } @@ -1396,6 +1410,14 @@ namespace Barotrauma.Lights return; } + foreach (var visibleConvexHull in visibleConvexHulls) + { + foreach (var convexHullList in convexHullsInRange) + { + convexHullList.IsHidden.Remove(visibleConvexHull); + } + } + CalculateLightVertices(verts); LastRecalculationTime = (float)Timing.TotalTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 2170143c5..222d4cae3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -22,7 +22,7 @@ namespace Barotrauma foreach (MapEntity e in linkedTo) { - bool isLinkAllowed = e is Item item && item.HasTag("dock"); + bool isLinkAllowed = e is Item item && item.HasTag(Tags.DockingPort); GUI.DrawLine(spriteBatch, new Vector2(WorldPosition.X, -WorldPosition.Y), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 151e15a14..e487bb33d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -25,7 +25,7 @@ namespace Barotrauma public event Action Resized; - private static bool resizing; + public static bool Resizing { get; private set; } private int resizeDirX, resizeDirY; private Rectangle? prevRect; @@ -122,11 +122,11 @@ namespace Barotrauma /// public static void UpdateSelecting(Camera cam) { - if (resizing) + if (Resizing) { if (!SelectedAny) { - resizing = false; + Resizing = false; } return; } @@ -243,7 +243,7 @@ namespace Barotrauma } else { - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in MapEntityList) { if (!e.SelectableInEditor) { continue; } if (e.IsMouseOn(position)) @@ -352,7 +352,7 @@ namespace Barotrauma selectionSize.X = position.X - selectionPos.X; selectionSize.Y = selectionPos.Y - position.Y; - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in MapEntityList) { entity.IsIncludedInSelection = false; } @@ -454,7 +454,7 @@ namespace Barotrauma selectionPos = Vector2.Zero; selectionSize = Vector2.Zero; - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in MapEntityList) { entity.IsIncludedInSelection = false; } @@ -525,7 +525,7 @@ namespace Barotrauma if (!isShiftDown) { return null; } - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in MapEntityList) { if (!e.SelectableInEditor || e is not Item potentialContainer) { continue; } @@ -730,7 +730,7 @@ namespace Barotrauma static partial void UpdateAllProjSpecific(float deltaTime) { - var entitiesToRender = Submarine.VisibleEntities ?? mapEntityList; + var entitiesToRender = Submarine.VisibleEntities ?? MapEntityList; foreach (MapEntity me in entitiesToRender) { if (me is Item item) @@ -832,35 +832,39 @@ namespace Barotrauma } if (selectionPos != Vector2.Zero) { - var (sizeX, sizeY) = selectionSize; - var (posX, posY) = selectionPos; - - posY = -posY; - - Vector2[] corners = - { - new Vector2(posX, posY), - new Vector2(posX + sizeX, posY), - new Vector2(posX + sizeX, posY + sizeY), - new Vector2(posX, posY + sizeY) - }; - - Color selectionColor = GUIStyle.Blue; - float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); - - GUI.DrawFilledRectangle(spriteBatch, corners[0], selectionSize, selectionColor * 0.1f); - - Vector2 offset = new Vector2(0f, thickness / 2f); - - if (sizeY < 0) { offset.Y = -offset.Y; } - - spriteBatch.DrawLine(corners[0], corners[1], selectionColor, thickness); - spriteBatch.DrawLine(corners[1] - offset, corners[2] + offset, selectionColor, thickness); - spriteBatch.DrawLine(corners[2], corners[3], selectionColor, thickness); - spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, selectionColor, thickness); + DrawSelectionRect(spriteBatch, selectionPos, selectionSize, GUIStyle.Blue); } } + public static void DrawSelectionRect(SpriteBatch spriteBatch, Vector2 pos, Vector2 size, Color color) + { + var (sizeX, sizeY) = size; + var (posX, posY) = pos; + + posY = -posY; + + Vector2[] corners = + { + new Vector2(posX, posY), + new Vector2(posX + sizeX, posY), + new Vector2(posX + sizeX, posY + sizeY), + new Vector2(posX, posY + sizeY) + }; + + float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); + + GUI.DrawFilledRectangle(spriteBatch, corners[0], size, color * 0.1f); + + Vector2 offset = new Vector2(0f, thickness / 2f); + + if (sizeY < 0) { offset.Y = -offset.Y; } + + spriteBatch.DrawLine(corners[0], corners[1], color, thickness); + spriteBatch.DrawLine(corners[1] - offset, corners[2] + offset, color, thickness); + spriteBatch.DrawLine(corners[2], corners[3], color, thickness); + spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, color, thickness); + } + public static List FilteredSelectedList { get; private set; } = new List(); public static void UpdateEditor(Camera cam, float deltaTime) @@ -995,10 +999,10 @@ namespace Barotrauma { if (CopiedList.Count == 0) { return; } - List prevEntities = new List(mapEntityList); + List prevEntities = new List(MapEntityList); Clone(CopiedList); - var clones = mapEntityList.Except(prevEntities).ToList(); + var clones = MapEntityList.Except(prevEntities).ToList(); var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); if (!nonWireClones.Any()) { nonWireClones = clones; } @@ -1027,12 +1031,12 @@ namespace Barotrauma /// public static List CopyEntities(List entities) { - List prevEntities = new List(mapEntityList); + List prevEntities = new List(MapEntityList); CopiedList = Clone(entities); //find all new entities created during cloning - var newEntities = mapEntityList.Except(prevEntities).ToList(); + var newEntities = MapEntityList.Except(prevEntities).ToList(); //do a "shallow remove" (removes the entities from the game without removing links between them) // -> items will stay in their containers @@ -1089,37 +1093,36 @@ namespace Barotrauma public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } + float ResizeHandleSize => 10 * GUI.Scale; + float ResizeHandleHighlightDistance => 8 * GUI.Scale; + private void UpdateResizing(Camera cam) { IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; - int StartY = ResizeVertical ? -1 : 0; + int startY = ResizeVertical ? -1 : 0; for (int x = startX; x < 2; x += 2) { - for (int y = StartY; y < 2; y += 2) + for (int y = startY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f + 5), y * (rect.Height * 0.5f + 5))); - - bool highlighted = Vector2.Distance(PlayerInput.MousePosition, handlePos) < 5.0f; + Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; if (highlighted && PlayerInput.PrimaryMouseButtonDown()) { selectionPos = Vector2.Zero; resizeDirX = x; resizeDirY = y; - resizing = true; + Resizing = true; startMovingPos = Vector2.Zero; - foreach (var mapEntity in mapEntityList) - { - if (mapEntity != this) { mapEntity.isHighlighted = false; } - } + ClearHighlightedEntities(); } } } - if (resizing) + if (Resizing) { if (prevRect == null) { @@ -1129,7 +1132,7 @@ namespace Barotrauma Vector2 placePosition = new Vector2(rect.X, rect.Y); Vector2 placeSize = new Vector2(rect.Width, rect.Height); - Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); + Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub, round: true); if (PlayerInput.IsShiftDown()) { @@ -1145,8 +1148,8 @@ namespace Barotrauma { mousePos.X = Math.Min(mousePos.X, rect.Right - Submarine.GridSize.X); - placeSize.X = (placePosition.X + placeSize.X) - mousePos.X; - placePosition.X = mousePos.X; + placeSize.X = MathF.Round((placePosition.X + placeSize.X) - mousePos.X); + placePosition.X = MathF.Round(mousePos.X); } if (resizeDirY < 0) { @@ -1168,7 +1171,7 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld()) { - resizing = false; + Resizing = false; Resized?.Invoke(rect); if (prevRect != null) { @@ -1201,13 +1204,16 @@ namespace Barotrauma { for (int y = StartY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f + 5), y * (rect.Height * 0.5f + 5))); - - bool highlighted = Vector2.Distance(PlayerInput.MousePosition, handlePos) < 5.0f; + Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; + if (highlighted && !PlayerInput.PrimaryMouseButtonHeld()) + { + GUI.MouseCursor = CursorState.Hand; + } GUI.DrawRectangle(spriteBatch, - handlePos - new Vector2(3.0f, 3.0f), - new Vector2(6.0f, 6.0f), + handlePos - new Vector2(ResizeHandleSize / 2), + new Vector2(ResizeHandleSize), Color.White * (highlighted ? 1.0f : 0.6f), true, 0, (int)Math.Max(1.5f / GameScreen.Selected.Cam.Zoom, 1.0f)); @@ -1224,7 +1230,7 @@ namespace Barotrauma Rectangle selectionRect = Submarine.AbsRect(pos, size); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in MapEntityList) { if (!entity.SelectableInEditor) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 9f1b8c657..3c28cbe17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -56,15 +56,43 @@ namespace Barotrauma if (!CastShadow) { return; } convexHulls ??= new List(); - var h = new ConvexHull( - new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), - IsHorizontal, - this); - if (Math.Abs(rotation) > 0.001f) + + //if the convex hull is longer than this, we need to split it to multiple parts + //very large convex hulls don't play nicely with the lighting or LOS, because the shadow cast + //by the convex hull would need to be extruded very far to cover the whole screen + const float MaxConvexHullLength = 1024.0f; + float length = IsHorizontal ? size.X : size.Y; + int convexHullCount = (int)Math.Max(1, Math.Ceiling(length / MaxConvexHullLength)); + + Vector2 sectionSize = size; + if (convexHullCount > 1) { - h.Rotate(position, rotation); + if (IsHorizontal) + { + sectionSize.X = length / convexHullCount; + } + else + { + sectionSize.Y = length / convexHullCount; + } + } + + for (int i = 0; i < convexHullCount; i++) + { + Vector2 offset = + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * + (i * length / convexHullCount); + + var h = new ConvexHull( + new Rectangle((position - size / 2 + offset).ToPoint(), sectionSize.ToPoint()), + IsHorizontal, + this); + if (Math.Abs(rotation) > 0.001f) + { + h.Rotate(position, rotation); + } + convexHulls.Add(h); } - convexHulls.Add(h); } public override void UpdateEditing(Camera cam, float deltaTime) @@ -498,7 +526,7 @@ namespace Barotrauma private bool ConditionalMatches(PropertyConditional conditional) { - if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 795401f2f..6d330e43e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -70,15 +71,16 @@ namespace Barotrauma if (visibleEntities == null) { - visibleEntities = new List(MapEntity.mapEntityList.Count); + visibleEntities = new List(MapEntity.MapEntityList.Count); } else { visibleEntities.Clear(); } - foreach (MapEntity entity in MapEntity.mapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { + if (entity == null || entity.Removed) { continue; } if (entity.Submarine != null) { if (!visibleSubs.Contains(entity.Submarine)) { continue; } @@ -102,7 +104,7 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch, bool editing = false) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -112,7 +114,7 @@ namespace Barotrauma public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -161,7 +163,7 @@ namespace Barotrauma private static readonly List depthSortedDamageable = new List(); public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; depthSortedDamageable.Clear(); @@ -200,7 +202,7 @@ namespace Barotrauma public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -220,7 +222,7 @@ namespace Barotrauma public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -501,7 +503,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.GetComponent() == null) { continue; } + if (item.GetComponent() == null) { continue; } if (!item.linkedTo.Any()) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.DisconnectedVents)) @@ -529,7 +531,7 @@ namespace Barotrauma warnings.Add(SubEditorScreen.WarningType.NoCargoSpawnpoints); } } - if (!Item.ItemList.Any(it => it.GetComponent() != null && it.HasTag("ballast"))) + if (Item.ItemList.None(it => it.GetComponent() != null && it.HasTag(Tags.Ballast))) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoBallastTag)) { @@ -537,6 +539,14 @@ namespace Barotrauma warnings.Add(SubEditorScreen.WarningType.NoBallastTag); } } + if (Item.ItemList.None(it => it.HasTag(Tags.HiddenItemContainer))) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHiddenContainers)) + { + errorMsgs.Add(TextManager.Get("NoHiddenContainersWarning").Value); + warnings.Add(SubEditorScreen.WarningType.NoHiddenContainers); + } + } } else if (Info.Type == SubmarineType.OutpostModule) { @@ -581,7 +591,7 @@ namespace Barotrauma } } - if ((MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold) + if ((MapEntity.MapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.StructureCount)) { @@ -641,7 +651,7 @@ namespace Barotrauma } } - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.MapEntityList) { if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000) { @@ -653,7 +663,7 @@ namespace Barotrauma } } - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.MapEntityList) { if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000) { @@ -703,12 +713,12 @@ namespace Barotrauma return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } - public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) + public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, bool round = false) { Vector2 position = PlayerInput.MousePosition; position = cam.ScreenToWorld(position); - Vector2 worldGridPos = VectorToWorldGrid(position); + Vector2 worldGridPos = VectorToWorldGrid(position, round); if (sub != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 8491dd736..e056cc997 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -9,7 +10,6 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma { @@ -26,22 +26,51 @@ namespace Barotrauma private GUIFrame previewFrame; + private sealed class LoadedHull + { + public UInt16 ID; + public readonly ImmutableList LinkedHulls; + public readonly Rectangle Rect; + public readonly Identifier NameIdentifier; + + public LoadedHull(XElement element) + { + ID = (ushort)element.GetAttributeInt("id", Entity.NullEntityID); + NameIdentifier = element.GetAttributeIdentifier("roomname", ""); + Rect = element.GetAttributeRect("rect", Rectangle.Empty); + Rect.Y = -Rect.Y; + LinkedHulls = element.GetAttributeUshortArray("linked", Array.Empty()).ToImmutableList(); + } + } + private sealed class HullCollection { - public readonly List Rects; + public readonly List Hulls = new List(); + public readonly List Rects = new List(); public readonly LocalizedString Name; - public HullCollection(Identifier identifier) + public HullCollection(LoadedHull hull) { - Rects = new List(); - Name = TextManager.Get(identifier).Fallback(identifier.Value); + Name = TextManager.Get(hull.NameIdentifier).Fallback(hull.NameIdentifier.ToString()); + AddHull(hull); } - public void AddRect(XElement element) + public void AddHull(LoadedHull hull) { - Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); - rect.Y = -rect.Y; - Rects.Add(rect); + Hulls.Add(hull); + Rects.Add(hull.Rect); + } + + private bool Contains(UInt16 hullId) + { + return Hulls.Any(h => h.ID == hullId); + } + + public bool IsLinkedTo(HullCollection other) + { + return + Hulls.Any(h => h.LinkedHulls.Any(id => other.Contains(id))) || + other.Hulls.Any(h => h.LinkedHulls.Any(id => Contains(id))); } } @@ -56,7 +85,7 @@ namespace Barotrauma } } - private readonly Dictionary hullCollections; + private readonly List hullCollections = new List(); private readonly List doors; private static SubmarinePreview instance = null; @@ -80,7 +109,7 @@ namespace Barotrauma isDisposed = false; loadTask = null; - hullCollections = new Dictionary(); + hullCollections = new List(); doors = new List(); previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); @@ -253,7 +282,7 @@ namespace Barotrauma } var wireNodes = new List(); - + List loadedHulls = new List(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } @@ -275,12 +304,7 @@ namespace Barotrauma Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); if (!identifier.IsEmpty) { - if (!hullCollections.TryGetValue(identifier, out HullCollection hullCollection)) - { - hullCollection = new HullCollection(identifier); - hullCollections.Add(identifier, hullCollection); - } - hullCollection.AddRect(subElement); + loadedHulls.Add(new LoadedHull(subElement)); } break; } @@ -288,6 +312,34 @@ namespace Barotrauma await Task.Yield(); } + //List tempHullCollections = new List(); + foreach (LoadedHull hull in loadedHulls) + { + hullCollections.Add(new HullCollection(hull)); + } + + bool intersectionFound; + do + { + intersectionFound = false; + for (int i = 0; i < hullCollections.Count; i++) + { + for (int j = i + 1; j < hullCollections.Count; j++) + { + var collection1 = hullCollections[i]; + var collection2 = hullCollections[j]; + if (collection1.IsLinkedTo(collection2)) + { + collection2.Hulls.ForEach(h => collection1.AddHull(h)); + hullCollections.Remove(collection2); + intersectionFound = true; + break; + } + } + if (intersectionFound) { break; } + } + } while (intersectionFound); + bounds = (spriteRecorder.Min, spriteRecorder.Max); wireNodes.ForEach(BakeWireNodes); @@ -682,7 +734,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.BackToFront, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; - foreach (var hullCollection in hullCollections.Values) + foreach (var hullCollection in hullCollections) { bool mouseOver = false; if (GUI.MouseOn == null || GUI.MouseOn == component) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index f86c9cd48..1a9fb2c71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -538,7 +538,7 @@ namespace Barotrauma.Networking { string extension = Path.GetExtension(file); if ((!extension.Equals(".sub", StringComparison.OrdinalIgnoreCase) - && !file.Equals("gamesession.xml")) + && !file.Equals(SaveUtil.GameSessionFileName)) || file.CleanUpPathCrossPlatform(correctFilenameCase: false).Contains('/')) { ErrorMessage = $"Found unexpected file in \"{fileTransfer.FileName}\"! ({file})"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index bf6adf1cf..36740fb62 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -112,9 +112,8 @@ namespace Barotrauma.Networking //has the client been given a character to control this round public bool HasSpawned; - public bool SpawnAsTraitor; public LocalizedString TraitorFirstObjective; - public TraitorMissionPrefab TraitorMission = null; + public TraitorEventPrefab TraitorMission = null; public byte SessionId { get; private set; } @@ -404,7 +403,7 @@ namespace Barotrauma.Networking if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId) { serverDisplayName = steamId.ToString(); - string steamUserName = Steamworks.SteamFriends.GetFriendPersonaName(steamId.Value); + string steamUserName = new Steamworks.Friend(steamId.Value).Name; if (!string.IsNullOrEmpty(steamUserName) && steamUserName != "[unknown]") { serverDisplayName = steamUserName; @@ -775,15 +774,15 @@ namespace Barotrauma.Networking } } - byte traitorCount = inc.ReadByte(); - List traitorResults = new List(); - for (int i = 0; i(inc); } roundInitStatus = RoundInitStatus.Interrupted; - CoroutineManager.StartCoroutine(EndGame(endMessage, traitorResults, transitionType), "EndGame"); + CoroutineManager.StartCoroutine(EndGame(endMessage, transitionType, traitorResults), "EndGame"); GUI.SetSavingIndicatorState(save); break; case ServerPacketHeader.CAMPAIGN_SETUP_INFO: @@ -829,18 +828,21 @@ namespace Barotrauma.Networking case ServerPacketHeader.MEDICAL: campaign?.MedicalClinic?.ClientRead(inc); break; + case ServerPacketHeader.CIRCUITBOX: + ReadCircuitBoxMessage(inc); + break; case ServerPacketHeader.MONEY: campaign?.ClientReadMoney(inc); break; case ServerPacketHeader.READY_CHECK: ReadyCheck.ClientRead(inc); break; + case ServerPacketHeader.TRAITOR_MESSAGE: + TraitorManager.ClientRead(inc); + break; case ServerPacketHeader.FILE_TRANSFER: FileReceiver.ReadMessage(inc); break; - case ServerPacketHeader.TRAITOR_MESSAGE: - ReadTraitorMessage(inc); - break; case ServerPacketHeader.MISSION: { int missionIndex = inc.ReadByte(); @@ -985,7 +987,7 @@ namespace Barotrauma.Networking if (disconnectPacket.IsEventSyncError) { GameMain.NetLobbyScreen.Select(); - GameMain.GameSession?.EndRound("", null); + GameMain.GameSession?.EndRound(""); GameStarted = false; myCharacter = null; } @@ -1179,44 +1181,20 @@ namespace Barotrauma.Networking } } - private void ReadTraitorMessage(IReadMessage inc) + private static void ReadCircuitBoxMessage(IReadMessage inc) { - TraitorMessageType messageType = (TraitorMessageType)inc.ReadByte(); - string missionIdentifier = inc.ReadString(); - string messageFmt = inc.ReadString(); - LocalizedString message = TextManager.GetServerMessage(messageFmt); + var header = INetSerializableStruct.Read(inc); - var missionPrefab = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == missionIdentifier); - Sprite icon = missionPrefab?.Icon; - - switch (messageType) + INetSerializableStruct data = header.Opcode switch { - case TraitorMessageType.Objective: - var isTraitor = !message.IsNullOrEmpty(); - SpawnAsTraitor = isTraitor; - TraitorFirstObjective = message; - TraitorMission = missionPrefab; - if (Character != null) - { - Character.IsTraitor = isTraitor; - Character.TraitorCurrentObjective = message; - } - break; - case TraitorMessageType.Console: - GameMain.Client.AddChatMessage(ChatMessage.Create("", message.Value, ChatMessageType.Console, null)); - DebugConsole.NewMessage(message); - break; - case TraitorMessageType.ServerMessageBox: - var msgBox = new GUIMessageBox("", message, Array.Empty(), type: GUIMessageBox.Type.InGame, icon: icon); - if (msgBox.Icon != null) - { - msgBox.IconColor = missionPrefab.IconColor; - } - break; - case TraitorMessageType.Server: - default: - GameMain.Client.AddChatMessage(message.Value, ChatMessageType.Server); - break; + CircuitBoxOpcode.Cursor => INetSerializableStruct.Read(inc), + CircuitBoxOpcode.Error => INetSerializableStruct.Read(inc), + _ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.") + }; + + if (header.FindTarget().TryUnwrap(out CircuitBox box)) + { + box.ClientRead(data); } } @@ -1372,7 +1350,6 @@ namespace Barotrauma.Networking ServerSettings.AllowRewiring = inc.ReadBoolean(); ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); - ServerSettings.AllowRagdollButton = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); @@ -1698,7 +1675,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - public IEnumerable EndGame(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public IEnumerable EndGame(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { //round starting up, wait for it to finish DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 60); @@ -1717,14 +1694,13 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType); + GameMain.GameSession?.EndRound(endMessage, transitionType, traitorResults); ServerSettings.ServerDetailsChanged = true; GameStarted = false; Character.Controlled = null; WaitForNextRoundRespawn = null; - SpawnAsTraitor = false; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; RespawnManager = null; @@ -1976,7 +1952,9 @@ namespace Barotrauma.Networking bool allowSpectating = inc.ReadBoolean(); - YesNoMaybe traitorsEnabled = (YesNoMaybe)inc.ReadRangedInteger(0, 2); + float traitorProbability = inc.ReadSingle(); + int traitorDangerLevel = inc.ReadRangedInteger(TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); + MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All); int modeIndex = inc.ReadByte(); @@ -2020,7 +1998,9 @@ namespace Barotrauma.Networking if (!allowSubVoting) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); - GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); + GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel); + GameMain.NetLobbyScreen.SetMissionType(missionType); GameMain.NetLobbyScreen.SelectMode(modeIndex); @@ -2498,9 +2478,9 @@ namespace Barotrauma.Networking break; case FileTransferType.CampaignSave: - XDocument gameSessionDoc = SaveUtil.LoadGameSessionDoc(transfer.FilePath); - byte campaignID = (byte)MathHelper.Clamp(gameSessionDoc.Root.GetAttributeInt("campaignid", 0), 0, 255); - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.CampaignID != campaignID) + XElement gameSessionDocRoot = SaveUtil.DecompressSaveAndLoadGameSessionDoc(transfer.FilePath)?.Root; + byte campaignID = (byte)MathHelper.Clamp(gameSessionDocRoot.GetAttributeInt("campaignid", 0), 0, 255); + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); @@ -2512,7 +2492,7 @@ namespace Barotrauma.Networking GameMain.GameSession.SavePath = transfer.FilePath; if (GameMain.GameSession.SubmarineInfo == null || campaign.Map == null) { - string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDoc.Root.GetAttributeString("submarine", "")) + ".sub"; + string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDocRoot.GetAttributeString("submarine", "")) + ".sub"; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } @@ -2528,7 +2508,8 @@ namespace Barotrauma.Networking if (Screen.Selected == GameMain.NetLobbyScreen) { - //reselect to refrest the state of the lobby screen (enable spectate button, etc) + //reselect to refresh the state of the lobby screen (enable spectate button, etc) + GameMain.NetLobbyScreen.SaveAppearance(); GameMain.NetLobbyScreen.Select(); } @@ -2606,7 +2587,7 @@ namespace Barotrauma.Networking public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { - msg.WriteBoolean(characterInfo == null); + msg.WriteBoolean(GameMain.NetLobbyScreen.Spectating); msg.WritePadBits(); if (characterInfo == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 3cff1e7b4..6718bcbf4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -53,7 +53,7 @@ namespace Barotrauma.Networking protected ConnectionInitialization initializationStep; public bool ContentPackageOrderReceived { get; set; } protected int passwordSalt; - protected Steamworks.AuthTicket? steamAuthTicket; + protected Option steamAuthTicket; private GUIMessageBox? passwordMsgBox; public bool WaitingForPassword @@ -82,7 +82,7 @@ namespace Barotrauma.Networking Initialization = ConnectionInitialization.SteamTicketAndVersion }; - if (steamAuthTicket is { Canceled: true }) + if (steamAuthTicket.TryUnwrap(out var authTicket) && authTicket is { Canceled: true }) { throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled."); } @@ -92,11 +92,7 @@ namespace Barotrauma.Networking Name = GameMain.Client.Name, OwnerKey = ownerKey, SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), - SteamAuthTicket = steamAuthTicket?.Data switch - { - null => Option.None(), - var ticketData => Option.Some(ticketData) - }, + SteamAuthTicket = steamAuthTicket.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None), GameVersion = GameMain.Version.ToString(), Language = GameSettings.CurrentConfig.Language.Value }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 8e599ee17..1ed7a4e53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Text; using Lidgren.Network; using Barotrauma.Steam; +using System.Net.Sockets; namespace Barotrauma.Networking { @@ -28,8 +29,12 @@ namespace Barotrauma.Networking netPeerConfiguration = new NetPeerConfiguration("barotrauma") { - UseDualModeSockets = GameSettings.CurrentConfig.UseDualModeSockets + DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; + if (endpoint.NetEndpoint.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + netPeerConfiguration.LocalAddress = System.Net.IPAddress.IPv6Any; + } netPeerConfiguration.DisableMessageType( NetIncomingMessageType.DebugMessage @@ -53,8 +58,8 @@ namespace Barotrauma.Networking if (SteamManager.IsInitialized) { - steamAuthTicket = SteamManager.GetAuthSessionTicket(); - if (steamAuthTicket == null) + steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint); + if (steamAuthTicket.IsNone()) { throw new Exception("GetAuthSessionTicket returned null"); } @@ -207,8 +212,8 @@ namespace Barotrauma.Networking netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation()); netClient = null; - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } + steamAuthTicket = Option.None; callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index e833fa7eb..08318d13f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Networking ContentPackageOrderReceived = false; - steamAuthTicket = SteamManager.GetAuthSessionTicket(); + steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint); //TODO: wait for GetAuthSessionTicketResponse_t if (steamAuthTicket == null) @@ -363,8 +363,8 @@ namespace Barotrauma.Networking Steamworks.SteamNetworking.ResetActions(); Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value); - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } + steamAuthTicket = Option.None; callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 30a3b78a8..5441869bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -49,11 +49,15 @@ namespace Barotrauma.Networking respawnPromptCoroutine = CoroutineManager.Invoke(() => { - if (Character.Controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } + if (Character.Controlled != null || (GameMain.GameSession is not { IsRunning: true })) { return; } - LocalizedString text = - TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)(SkillReductionOnDeath * 100)).ToString()) - + "\n\n" + TextManager.Get("respawnquestionprompt"); + LocalizedString text = TextManager.Get("respawnquestionprompt"); + if (SkillLossPercentageOnDeath > 0) + { + text = + TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)SkillLossPercentageOnDeath).ToString()) + + "\n\n" + text; + }; var respawnPrompt = new GUIMessageBox( TextManager.Get("tutorial.tryagainheader"), text, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index 23a9f6bef..f7c5d0a44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using System.Xml.Linq; using Barotrauma.Steam; +using System.Globalization; namespace Barotrauma.Networking { @@ -65,8 +66,8 @@ namespace Barotrauma.Networking [Serialize(false, IsPropertySaveable.Yes)] public bool AllowRespawn { get; set; } - [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] - public YesNoMaybe TraitorsEnabled { get; set; } + [Serialize(0.0f, IsPropertySaveable.Yes)] + public float TraitorProbability { get; set; } [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get; set; } @@ -397,10 +398,10 @@ namespace Barotrauma.Networking public IEnumerable GetPlayStyleTags() { yield return $"Karma.{KarmaEnabled}".ToIdentifier(); - yield return (TraitorsEnabled == YesNoMaybe.Yes ? $"Traitors.True" : $"Traitors.False").ToIdentifier(); + yield return (TraitorProbability > 0.0f ? $"Traitors.True" : $"Traitors.False").ToIdentifier(); yield return $"VoIP.{VoipEnabled}".ToIdentifier(); yield return $"FriendlyFire.{FriendlyFireEnabled}".ToIdentifier(); - yield return $"Modded.{ContentPackages.Any()}".ToIdentifier(); + yield return $"Modded.{IsModded}".ToIdentifier(); } public void UpdateInfo(Func valueGetter) @@ -424,7 +425,7 @@ namespace Barotrauma.Networking VoipEnabled = getBool("voicechatenabled"); GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; - if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } + if (float.TryParse(valueGetter("traitors"), NumberStyles.Any, CultureInfo.InvariantCulture, out float traitorProbability)) { TraitorProbability = traitorProbability; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index e0476a625..76036545e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -146,7 +146,9 @@ namespace Barotrauma.Networking List tickBoxes = new List(); foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) { - var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUIStyle.SmallFont) + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), + TextManager.Get("ServerLog." + messageTypeName[msgType]).Fallback(messageTypeName[msgType]), + font: GUIStyle.SmallFont) { Selected = true, TextColor = messageColor[msgType], diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 57c19e71c..82f553984 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -182,7 +182,8 @@ namespace Barotrauma.Networking int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, - int traitorSetting = 0, + float? traitorProbability = null, + int traitorDangerLevel = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) @@ -244,7 +245,11 @@ namespace Barotrauma.Networking { outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); - outMsg.WriteByte((byte)(traitorSetting + 1)); + + outMsg.WriteBoolean(traitorProbability != null); + outMsg.WriteSingle(traitorProbability ?? 0.0f); + outMsg.WriteByte((byte)(traitorDangerLevel + 1)); + outMsg.WriteByte((byte)(botCount + 1)); outMsg.WriteByte((byte)(botSpawnMode + 1)); @@ -557,6 +562,26 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); + LocalizedString skillLossLabel = TextManager.Get("ServerSettingsSkillLossPercentageOnDeath"); + var skillLossText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), roundsContent.RectTransform), skillLossLabel); + var skillLossSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), roundsContent.RectTransform), barSize: 0.1f, style: "GUISlider") + { + UserData = skillLossText, + Range = new Vector2(0, 100), + StepValue = 1, + OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = TextManager.AddPunctuation( + ':', + skillLossLabel, + TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString())); + return true; + } + }; + GetPropertyData(nameof(SkillLossPercentageOnDeath)).AssignGUIComponent(skillLossSlider); + skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); + var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")); GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); @@ -690,7 +715,7 @@ namespace Barotrauma.Networking Stretch = true }; - var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); + var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 2, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); var maximumTransferAmount = CreateLabeledNumberInput(numberLayout, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); @@ -701,9 +726,6 @@ namespace Barotrauma.Networking lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); GetPropertyData(nameof(LootedMoneyDestination)).AssignGUIComponent(lootedMoneyDestination); - var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); - GetPropertyData(nameof(AllowRagdollButton)).AssignGUIComponent(ragdollButtonBox); - var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 470219c9f..9ca9c34f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -188,6 +188,10 @@ namespace Barotrauma msg.WriteBoolean(false); //not initiating a vote msg.WriteInt32(money); break; + case VoteType.Traitor: + //use 0 to indicate we voted for no-one + msg.WriteInt32((data as Client)?.SessionId ?? 0); + break; } msg.WritePadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 3f8492120..9d762e0d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -85,7 +85,7 @@ namespace Barotrauma.Particles public float StartDelay { get { return startDelay; } - set { startDelay = MathHelper.Clamp(value, Prefab.StartDelayMin, prefab.StartDelayMax); } + set { startDelay = Math.Max(value, 0.0f); } } public Vector2 Size @@ -311,7 +311,8 @@ namespace Barotrauma.Particles { foreach (ParticleEmitter emitter in subEmitters) { - emitter.Emit(deltaTime, position, currentHull); + emitter.Emit(deltaTime, position, currentHull, particleRotation: rotation, + sizeMultiplier: emitter.Prefab.Properties.CopyParentParticleScale ? Math.Max(size.X, size.Y) : 1.0f); } } @@ -566,11 +567,12 @@ namespace Barotrauma.Particles drawPosition = Timing.Interpolate(prevPosition, position); drawRotation = Timing.Interpolate(prevRotation, rotation); } - + public void Draw(SpriteBatch spriteBatch) { - Vector2 drawSize = size; + if (startDelay > 0.0f) { return; } + Vector2 drawSize = size; if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime) { drawSize *= MathUtils.SmoothStep((totalLifeTime - lifeTime) / prefab.GrowTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 55d0d15cd..4bd6e7605 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -88,6 +88,9 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")] public bool CopyTargetAngle { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for particles spawned by another particle. Makes the emitter copy the scale of the parent particle.")] + public bool CopyParentParticleScale { get; set; } + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } @@ -195,7 +198,11 @@ namespace Barotrauma.Particles private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) { var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; - if (particlePrefab == null) { return; } + if (particlePrefab == null) + { + DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\"."); + return; + } Vector2 velocity = Vector2.Zero; if (!MathUtils.NearlyEqual(Prefab.Properties.VelocityMax * velocityMultiplier, 0.0f) || !MathUtils.NearlyEqual(Prefab.Properties.DistanceMax, 0.0f)) @@ -206,7 +213,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, particlePrefab.DrawOnTop || Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { @@ -227,6 +234,7 @@ namespace Barotrauma.Particles public Rectangle CalculateParticleBounds(Vector2 startPosition) { Rectangle bounds = new Rectangle((int)startPosition.X, (int)startPosition.Y, (int)startPosition.X, (int)startPosition.Y); + if (Prefab.ParticlePrefab == null) { return bounds; } for (float angle = Prefab.Properties.AngleMinRad; angle <= Prefab.Properties.AngleMaxRad; angle += 0.1f) { @@ -255,16 +263,22 @@ namespace Barotrauma.Particles } bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y); - return bounds; } } class ParticleEmitterPrefab { - private readonly Identifier particlePrefabName; + public readonly Identifier ParticlePrefabName; - public ParticlePrefab ParticlePrefab => ParticlePrefab.Prefabs[particlePrefabName]; + public ParticlePrefab ParticlePrefab + { + get + { + ParticlePrefab.Prefabs.TryGet(ParticlePrefabName, out var prefab); + return prefab; + } + } public readonly ParticleEmitterProperties Properties; @@ -273,13 +287,13 @@ namespace Barotrauma.Particles public ParticleEmitterPrefab(ContentXElement element) { Properties = new ParticleEmitterProperties(element); - particlePrefabName = element.GetAttributeIdentifier("particle", ""); + ParticlePrefabName = element.GetAttributeIdentifier("particle", ""); } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) { Properties = properties; - particlePrefabName = prefab.Identifier; + ParticlePrefabName = prefab.Identifier; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 9aa60c592..37c906798 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -43,6 +43,13 @@ namespace Barotrauma.Particles } private Particle[] particles; + /// + /// Used for rendering the particles in the order in which they were created (starting from the most recent one) + /// to avoid the order of the particles shuffling around when particles are removed from the pool. + /// Linked list for fast additions and removals at the middle of the list. + /// + private readonly LinkedList particlesInCreationOrder = new LinkedList(); + private Camera cam; public Camera Camera @@ -75,7 +82,7 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } if (particleCount >= MaxParticles) @@ -116,12 +123,13 @@ namespace Barotrauma.Particles } if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } + Particle particle = particles[particleCount]; - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, prefab.DrawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); - + particle.Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; + particlesInCreationOrder.AddFirst(particle); - return particles[particleCount - 1]; + return particle; } public static List GetPrefabList() @@ -137,11 +145,10 @@ namespace Barotrauma.Particles private void RemoveParticle(int index) { + particlesInCreationOrder.Remove(particles[index]); particleCount--; - Particle swap = particles[index]; - particles[index] = particles[particleCount]; - particles[particleCount] = swap; + (particles[particleCount], particles[index]) = (particles[index], particles[particleCount]); } @@ -201,9 +208,8 @@ namespace Barotrauma.Particles { ParticlePrefab.DrawTargetType drawTarget = inWater ? ParticlePrefab.DrawTargetType.Water : ParticlePrefab.DrawTargetType.Air; - for (int i = 0; i < particleCount; i++) + foreach (var particle in particlesInCreationOrder) { - var particle = particles[i]; if (particle.BlendState != blendState) { continue; } //equivalent to !particles[i].DrawTarget.HasFlag(drawTarget) but garbage free and faster if ((particle.DrawTarget & drawTarget) == 0) { continue; } @@ -215,14 +221,15 @@ namespace Barotrauma.Particles continue; } } - - particles[i].Draw(spriteBatch); + + particle.Draw(spriteBatch); } } public void ClearParticles() { particleCount = 0; + particlesInCreationOrder.Clear(); } public void RemoveByPrefab(ParticlePrefab prefab) @@ -234,6 +241,7 @@ namespace Barotrauma.Particles { if (i < particleCount) { particleCount--; } + particlesInCreationOrder.Remove(particles[particleCount]); Particle swap = particles[particleCount]; particles[particleCount] = null; particles[i] = swap; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 23fb1ea3d..c550fbfee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -165,7 +165,7 @@ namespace Barotrauma.Particles [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMax { get; private set; } - [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How many seconds it takes for the particle to grow to it's initial size.")] + [Editable(minValue: 0, maxValue: float.MaxValue, decimals: 2), Serialize(0.0f, IsPropertySaveable.No, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index 8f61bf354..e8445be17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -28,7 +28,7 @@ namespace Barotrauma scale, color, Dir < 0, invert); } - public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) + public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false, Vector2? origin = null) { if (!Enabled) { return; } UpdateDrawPosition(); @@ -42,7 +42,7 @@ namespace Barotrauma { spriteEffect |= SpriteEffects.FlipVertically; } - sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color, -drawRotation, scale, spriteEffect, depth); + sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color, origin ?? sprite.Origin, - drawRotation, scale, spriteEffect, depth); } public void DebugDraw(SpriteBatch spriteBatch, Color color, bool forceColor = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 1440b125b..a3d44b5d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -132,7 +132,7 @@ namespace Barotrauma try { - if (exception is GameMain.LoadingException) + if (exception.StackTrace.Contains("Barotrauma.GameMain.Load")) { //exception occurred in loading screen: //assume content packages are the culprit and reset them diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index bb8f98570..ec8146ae0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -421,7 +421,7 @@ namespace Barotrauma } } - public abstract void UpdateLoadMenu(IEnumerable saveFiles = null); + public abstract void CreateLoadMenu(IEnumerable saveFiles = null); protected bool DeleteSave(GUIButton button, object obj) { @@ -434,7 +434,7 @@ namespace Barotrauma { SaveUtil.DeleteSave(saveInfo.FilePath); prevSaveFiles?.RemoveAll(s => s.FilePath == saveInfo.FilePath); - UpdateLoadMenu(prevSaveFiles.ToList()); + CreateLoadMenu(prevSaveFiles.ToList()); return true; }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index c86f3c04e..0a4a9958e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -162,7 +162,7 @@ namespace Barotrauma verticalLayout.Recalculate(); - UpdateLoadMenu(saveFiles); + CreateLoadMenu(saveFiles); } private IEnumerable WaitForCampaignSetup() @@ -192,7 +192,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public override void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void CreateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index f3e1cfdaa..6f51a78d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -20,11 +20,10 @@ namespace Barotrauma private GUIButton nextButton; private GUIListBox characterInfoColumns; - public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) : base(newGameContainer, loadGameContainer) { - UpdateNewGameMenu(submarines); - UpdateLoadMenu(saveFiles); + CreateNewGameMenu(); } private int currentPage = 0; @@ -73,7 +72,7 @@ namespace Barotrauma }); } - private void UpdateNewGameMenu(IEnumerable submarines) + private void CreateNewGameMenu() { pageContainer = new GUIListBox(new RectTransform(Vector2.One, newGameContainer.RectTransform), style: null, isHorizontal: true) @@ -92,7 +91,7 @@ namespace Barotrauma Anchor.Center)); } - CreateFirstPage(createPageLayout(), submarines); + CreateFirstPage(createPageLayout()); CreateSecondPage(createPageLayout()); pageContainer.RecalculateChildren(); @@ -108,7 +107,7 @@ namespace Barotrauma SetPage(0); } - private void CreateFirstPage(GUILayoutGroup firstPageLayout, IEnumerable submarines) + private void CreateFirstPage(GUILayoutGroup firstPageLayout) { firstPageLayout.RelativeSpacing = 0.02f; @@ -238,8 +237,6 @@ namespace Barotrauma columnContainer.Recalculate(); leftColumn.Recalculate(); rightColumn.Recalculate(); - - if (submarines != null) { UpdateSubList(submarines); } } private void CreateSecondPage(GUILayoutGroup secondPageLayout) @@ -578,7 +575,7 @@ namespace Barotrauma } } - public override void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void CreateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -625,21 +622,21 @@ namespace Barotrauma var saveFrame = CreateSaveElement(saveInfo); if (saveFrame == null) { continue; } - XDocument doc = SaveUtil.LoadGameSessionDoc(saveInfo.FilePath); + XElement docRoot = SaveUtil.ExtractGameSessionRootElementFromSaveFile(saveInfo.FilePath); - if (doc?.Root == null) + if (docRoot == null) { DebugConsole.ThrowError("Error loading save file \"" + saveInfo.FilePath + "\". The file may be corrupted."); saveFrame.GetChild().TextColor = GUIStyle.Red; continue; } - if (doc.Root.GetChildElement("multiplayercampaign") != null) + if (docRoot.GetChildElement("multiplayercampaign") != null) { //multiplayer campaign save in the wrong folder -> don't show the save saveList.Content.RemoveChild(saveFrame); continue; } - if (!SaveUtil.IsSaveFileCompatible(doc)) + if (!SaveUtil.IsSaveFileCompatible(docRoot)) { saveFrame.GetChild().TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); @@ -668,14 +665,14 @@ namespace Barotrauma string fileName = saveInfo.FilePath; - XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); - if (doc?.Root == null) + XElement docRoot = SaveUtil.ExtractGameSessionRootElementFromSaveFile(fileName); + if (docRoot == null) { DebugConsole.ThrowError("Error loading save file \"" + fileName + "\". The file may be corrupted."); return false; } - loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(doc); + loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(docRoot); RemoveSaveFrame(); @@ -684,7 +681,7 @@ namespace Barotrauma .Select(t => (LocalizedString)t.ToLocalUserString()) .Fallback(TextManager.Get("Unknown")); - string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); + string mapseed = docRoot.GetAttributeString("mapseed", "unknown"); var saveFileFrame = new GUIFrame( new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index b3e163a16..f93163433 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1187,6 +1187,11 @@ namespace Barotrauma.CharacterEditor private void CreateLimb(ContentXElement newElement) { + if (RagdollParams.MainElement == null) + { + DebugConsole.ThrowError("Main element null! Failed to create a limb."); + return; + } var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastElement != null) { @@ -1217,6 +1222,11 @@ namespace Barotrauma.CharacterEditor DebugConsole.ThrowError(GetCharacterEditorTranslation("ExistingJointFound").Replace("[limbid1]", fromLimb.ToString()).Replace("[limbid2]", toLimb.ToString())); return; } + if (RagdollParams.MainElement == null) + { + DebugConsole.ThrowError("The main element of the ragdoll params is null! Failed to create a joint."); + return; + } //RagdollParams.StoreState(); Vector2 a1 = anchor1 ?? Vector2.Zero; Vector2 a2 = anchor2 ?? Vector2.Zero; @@ -1775,7 +1785,7 @@ namespace Barotrauma.CharacterEditor string ragdollPath = RagdollParams.GetDefaultFile(name, contentPackage); RagdollParams ragdollParams = isHumanoid ? RagdollParams.CreateDefault(ragdollPath, name, ragdoll) - : RagdollParams.CreateDefault(ragdollPath, name, ragdoll) as RagdollParams; + : RagdollParams.CreateDefault(ragdollPath, name, ragdoll); // Animations AnimationParams.ClearCache(); @@ -1789,6 +1799,7 @@ namespace Barotrauma.CharacterEditor foreach (var animation in animations) { XElement element = animation.MainElement; + if (element == null) { continue; } element.SetAttributeValue("type", name); string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType); element.Name = AnimationParams.GetDefaultFileName(name, animation.AnimationType); @@ -2140,7 +2151,8 @@ namespace Barotrauma.CharacterEditor { foreach (var limb in character.AnimController.Limbs) { - limb.ActiveSprite.ReloadTexture(); + if (limb == null) { continue; } + limb.ActiveSprite?.ReloadTexture(); limb.WearingItems.ForEach(i => i.Sprite.ReloadTexture()); limb.OtherWearables.ForEach(w => w.Sprite.ReloadTexture()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index 5383d7614..c5424e4ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -25,7 +25,7 @@ namespace Barotrauma public bool CanAddConnections { get; set; } - public readonly List Connections = new List(); + public readonly List Connections = new List(); public readonly List RemovableTypes = new List(); @@ -47,7 +47,7 @@ namespace Barotrauma public XElement SaveConnections() { XElement allConnections = new XElement("Connections", new XAttribute("i", ID)); - foreach (NodeConnection connection in Connections) + foreach (EventEditorNodeConnection connection in Connections) { XElement connectionElement = new XElement("Connection"); connectionElement.Add(new XAttribute("i", connection.ID)); @@ -93,12 +93,12 @@ namespace Barotrauma if (id < 0) { continue; } - NodeConnection? connection = Connections.Find(c => c.ID == id); + EventEditorNodeConnection? connection = Connections.Find(c => c.ID == id); if (connection == null) { if (string.Equals(connectionType, NodeConnectionType.Option.Label, StringComparison.InvariantCultureIgnoreCase)) { - connection = new NodeConnection(this, NodeConnectionType.Option) { ID = id, EndConversation = endConversation }; + connection = new EventEditorNodeConnection(this, NodeConnectionType.Option) { ID = id, EndConversation = endConversation }; Connections.Add(connection); } else @@ -143,7 +143,7 @@ namespace Barotrauma if (id2 < 0 || node < 0) { continue; } EditorNode? otherNode = EventEditorScreen.nodeList.Find(editorNode => editorNode.ID == node); - NodeConnection? otherConnection = otherNode?.Connections.Find(c => c.ID == id2); + EventEditorNodeConnection? otherConnection = otherNode?.Connections.Find(c => c.ID == id2); if (otherConnection != null) { connection.ConnectedTo.Add(otherConnection); @@ -184,20 +184,20 @@ namespace Barotrauma public void Connect(EditorNode otherNode, NodeConnectionType type) { - NodeConnection? conn = Connections.Find(connection => connection.Type == type && !connection.ConnectedTo.Any()); - NodeConnection? found = otherNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); + EventEditorNodeConnection? conn = Connections.Find(connection => connection.Type == type && !connection.ConnectedTo.Any()); + EventEditorNodeConnection? found = otherNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); if (found != null) { conn?.ConnectedTo.Add(found); } } - public void Connect(NodeConnection connection, NodeConnection ownConnection) + public static void Connect(EventEditorNodeConnection connection, EventEditorNodeConnection ownConnection) { connection.ConnectedTo.Add(ownConnection); } - public void Disconnect(NodeConnection conn) + public static void Disconnect(EventEditorNodeConnection conn) { foreach (var connection in EventEditorScreen.nodeList.SelectMany(editorNode => editorNode.Connections.Where(connection => connection.ConnectedTo.Contains(conn)))) { @@ -207,7 +207,7 @@ namespace Barotrauma public void ClearConnections() { - foreach (NodeConnection conn in Connections) + foreach (EventEditorNodeConnection conn in Connections) { conn.ClearConnections(); } @@ -218,7 +218,7 @@ namespace Barotrauma return Rectangle; } - public NodeConnection? GetConnectionOnMouse(Vector2 mousePos) + public EventEditorNodeConnection? GetConnectionOnMouse(Vector2 mousePos) { return Connections.FirstOrDefault(eventNodeConnection => eventNodeConnection.DrawRectangle.Contains(mousePos)); } @@ -254,7 +254,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, bodyRect, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom)); int x = 0, y = 0; - foreach (NodeConnection connection in Connections) + foreach (EventEditorNodeConnection connection in Connections) { switch (connection.Type.NodeSide) { @@ -275,10 +275,10 @@ namespace Barotrauma public virtual void AddOption() { - Connections.Add(new NodeConnection(this, NodeConnectionType.Option)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Option)); } - public void RemoveOption(NodeConnection connection) + public void RemoveOption(EventEditorNodeConnection connection) { int index = Connections.IndexOf(connection); foreach (var nodeConnection in Connections.Skip(index)) @@ -313,7 +313,7 @@ namespace Barotrauma foreach (EditorNode editorNode in EventEditorScreen.nodeList) { - List childConnection = editorNode.Connections.Where(connection => connection.Type == NodeConnectionType.Next || + List childConnection = editorNode.Connections.Where(connection => connection.Type == NodeConnectionType.Next || connection.Type == NodeConnectionType.Option || connection.Type == NodeConnectionType.Failure || connection.Type == NodeConnectionType.Success || @@ -338,18 +338,18 @@ namespace Barotrauma Size = new Vector2(256, 256); PropertyInfo[] properties = type.GetProperties().Where(info => info.CustomAttributes.Any(data => data.AttributeType == typeof(Serialize))).ToArray(); - Connections.Add(new NodeConnection(this, NodeConnectionType.Activate)); - Connections.Add(new NodeConnection(this, NodeConnectionType.Next)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Activate)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Next)); foreach (PropertyInfo property in properties) { - Connections.Add(new NodeConnection(this, NodeConnectionType.Value, property.Name, property.PropertyType, property)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Value, property.Name, property.PropertyType, property)); } if (IsInstanceOf(type, typeof(BinaryOptionAction))) { - Connections.Add(new NodeConnection(this, NodeConnectionType.Success)); - Connections.Add(new NodeConnection(this, NodeConnectionType.Failure)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Success)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Failure)); } if (IsInstanceOf(type, typeof(ConversationAction))) @@ -360,7 +360,7 @@ namespace Barotrauma if (IsInstanceOf(type, typeof(StatusEffectAction)) || IsInstanceOf(type, typeof(MissionAction))) { - Connections.Add(new NodeConnection(this, NodeConnectionType.Add)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Add)); } } @@ -395,7 +395,7 @@ namespace Barotrauma return ScaleRectFromConnections(Connections, Rectangle); } - public static Rectangle ScaleRectFromConnections(List connections, Rectangle baseRect) + public static Rectangle ScaleRectFromConnections(List connections, Rectangle baseRect) { // determine how big this box should get based on how many input/output nodes the sides have int y = connections.Count(connection => connection.Type.NodeSide == NodeConnectionType.Side.Left), @@ -409,15 +409,15 @@ namespace Barotrauma public Tuple[] GetOptions() { - IEnumerable myNode = Connections.Where(connection => connection.Type == NodeConnectionType.Option).ToArray(); + IEnumerable myNode = Connections.Where(connection => connection.Type == NodeConnectionType.Option).ToArray(); List> list = new List>(); if (myNode != null) { - foreach (NodeConnection connection in myNode) + foreach (EventEditorNodeConnection connection in myNode) { if (connection.ConnectedTo.Any()) { - foreach (NodeConnection nodeConnection in connection.ConnectedTo) + foreach (EventEditorNodeConnection nodeConnection in connection.ConnectedTo) { list.Add(Tuple.Create((EditorNode?) nodeConnection.Parent, connection.OptionText, connection.EndConversation)); } @@ -464,7 +464,7 @@ namespace Barotrauma Type = type; Value = type.IsValueType ? Activator.CreateInstance(type) : null; Size = new Vector2(256, 32); - Connections.Add(new NodeConnection(this, NodeConnectionType.Out, "Output", Type)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Out, "Output", Type)); } public override XElement Save() @@ -564,7 +564,7 @@ namespace Barotrauma Vector2 pos = GetDrawRectangle().Location.ToVector2() + (GetDrawRectangle().Size.ToVector2() / 2) - (valueTextSize / 2); Rectangle drawRect = Rectangle; drawRect.Inflate(-1, -1); - GUI.DrawString(spriteBatch, pos, WrappedText, NodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); + GUI.DrawString(spriteBatch, pos, WrappedText, EventEditorNodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); } } @@ -587,9 +587,9 @@ namespace Barotrauma { CanAddConnections = true; RemovableTypes.Add(NodeConnectionType.Value); - Connections.Add(new NodeConnection(this, NodeConnectionType.Activate)); - Connections.Add(new NodeConnection(this, NodeConnectionType.Next)); - Connections.Add(new NodeConnection(this, NodeConnectionType.Add)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Activate)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Next)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Add)); } public CustomNode() : this("Custom") @@ -605,7 +605,7 @@ namespace Barotrauma { Prompt(s => { - Connections.Add(new NodeConnection(this, NodeConnectionType.Value, s, typeof(string))); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Value, s, typeof(string))); return true; }); } @@ -617,7 +617,7 @@ namespace Barotrauma newElement.Add(new XAttribute("name", Name)); newElement.Add(new XAttribute("xpos", Position.X)); newElement.Add(new XAttribute("ypos", Position.Y)); - foreach (NodeConnection connection in Connections.FindAll(connection => connection.Type == NodeConnectionType.Value)) + foreach (EventEditorNodeConnection connection in Connections.FindAll(connection => connection.Type == NodeConnectionType.Value)) { newElement.Add(new XElement("Value", new XAttribute("name", connection.Attribute))); } @@ -634,7 +634,7 @@ namespace Barotrauma newNode.Position = new Vector2(posX, posY); foreach (XElement valueElement in element.Elements()) { - newNode.Connections.Add(new NodeConnection(newNode, NodeConnectionType.Value, valueElement.GetAttributeString("name", string.Empty), typeof(string))); + newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, valueElement.GetAttributeString("name", string.Empty), typeof(string))); } return newNode; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorNodeConnection.cs similarity index 86% rename from Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs rename to Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorNodeConnection.cs index d5814d94a..056913fb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorNodeConnection.cs @@ -39,7 +39,7 @@ namespace Barotrauma } } - internal class NodeConnection + internal class EventEditorNodeConnection { public string Attribute { get; } @@ -128,7 +128,7 @@ namespace Barotrauma public readonly EditorNode Parent; - public readonly List ConnectedTo = new List(); + public readonly List ConnectedTo = new List(); private readonly Color bgColor = Color.DarkGray * 0.8f; @@ -163,7 +163,7 @@ namespace Barotrauma ConnectedTo.Clear(); } - public NodeConnection(EditorNode parent, NodeConnectionType type, string attribute = "", Type? valueType = null, PropertyInfo? propertyInfo = null) + public EventEditorNodeConnection(EditorNode parent, NodeConnectionType type, string attribute = "", Type? valueType = null, PropertyInfo? propertyInfo = null) { Type = type; ValueType = valueType; @@ -244,7 +244,7 @@ namespace Barotrauma private void DrawConnections(SpriteBatch spriteBatch, int yOffset, float width = 2, Color? overrideColor = null) { - foreach (NodeConnection? eventNodeConnection in ConnectedTo) + foreach (EventEditorNodeConnection? eventNodeConnection in ConnectedTo) { if (eventNodeConnection != null) { @@ -279,37 +279,9 @@ namespace Barotrauma private void DrawSquareLine(SpriteBatch spriteBatch, Vector2 position, int yOffset, float width = 2, Color? overrideColor = null) { - // draw a line between 2 nodes using points - // the order of this array is messed up, I know - // order of points is from start node to end node: 0, 4, 1, 2, 5, 3 - Vector2[] points = new Vector2[6]; - int xOffset = 24 * (yOffset + 1); - points[0] = new Vector2(DrawRectangle.Right, DrawRectangle.Center.Y); - points[3] = position; - points[1] = points[0]; - points[2] = points[3]; - - points[4] = points[1]; - points[5] = points[2]; - - points[1].X += (points[2].X - points[1].X) / 2; - points[1].X = Math.Max(points[1].X, points[0].X + xOffset); - points[2].X = points[1].X; - - // if the node is "behind" us do some magic to make the line curve to prevent overlapping - if (points[1].X <= points[0].X + xOffset) - { - points[4].X += xOffset; - points[1].X = points[4].X; - points[1].Y += (points[2].Y - points[1].Y) / 2; - } - - if (points[2].X >= points[3].X - xOffset) - { - points[5].X -= xOffset; - points[2].X = points[5].X; - points[2].Y -= points[2].Y - points[1].Y; - } + float knobLength = 24 * (yOffset + 1); + Vector2 start = new Vector2(DrawRectangle.Right, DrawRectangle.Center.Y); + var (points, _) = ToolBox.GetSquareLineBetweenPoints(start, position, knobLength); Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUIStyle.Red; @@ -318,11 +290,11 @@ namespace Barotrauma drawColor = overrideColor.Value; } - GUI.DrawLine(spriteBatch, points[0], points[4], drawColor, width: (int)width); - GUI.DrawLine(spriteBatch, points[4], points[1], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[0], points[1], drawColor, width: (int)width); GUI.DrawLine(spriteBatch, points[1], points[2], drawColor, width: (int)width); - GUI.DrawLine(spriteBatch, points[2], points[5], drawColor, width: (int)width); - GUI.DrawLine(spriteBatch, points[5], points[3], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[2], points[3], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[3], points[4], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[4], points[5], drawColor, width: (int)width); } private static readonly Color defaultColor = new Color(139, 233, 253); @@ -345,7 +317,7 @@ namespace Barotrauma return color; } - public bool CanConnect(NodeConnection otherNode) + public bool CanConnect(EventEditorNodeConnection otherNode) { if (otherNode.OverrideValue != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index aee1762ac..9a733e235 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -25,7 +25,7 @@ namespace Barotrauma private readonly List selectedNodes = new List(); public static Vector2 DraggingPosition = Vector2.Zero; - public static NodeConnection? DraggedConnection; + public static EventEditorNodeConnection? DraggedConnection; private EditorNode? draggedNode; private Vector2 dragOffset; @@ -394,7 +394,7 @@ namespace Barotrauma newNode = new CustomNode(subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; foreach (XAttribute attribute in subElement.Attributes().Where(attribute => !attribute.ToString().StartsWith("_"))) { - newNode.Connections.Add(new NodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); + newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); } } @@ -414,7 +414,7 @@ namespace Barotrauma { if (xElement.Name.ToString().ToLowerInvariant() == "option") { - NodeConnection optionConnection = new NodeConnection(newNode, NodeConnectionType.Option) + EventEditorNodeConnection optionConnection = new EventEditorNodeConnection(newNode, NodeConnectionType.Option) { OptionText = xElement.GetAttributeString("text", string.Empty), EndConversation = xElement.GetAttributeBool("endconversation", false) @@ -423,7 +423,7 @@ namespace Barotrauma } } - foreach (NodeConnection connection in newNode.Connections) + foreach (EventEditorNodeConnection connection in newNode.Connections) { if (connection.Type == NodeConnectionType.Value) { @@ -479,8 +479,8 @@ namespace Barotrauma case "option": if (parent != null) { - NodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); - NodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection => + EventEditorNodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); + EventEditorNodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection => connection.Type == NodeConnectionType.Option && string.Equals(connection.OptionText, parentElement.GetAttributeString("text", string.Empty), StringComparison.Ordinal)); if (activateConnection != null) @@ -671,7 +671,7 @@ namespace Barotrauma } } - private void CreateContextMenu(EditorNode node, NodeConnection? connection = null) + private void CreateContextMenu(EditorNode node, EventEditorNodeConnection? connection = null) { if (GUIContextMenu.CurrentContextMenu != null) { return; } @@ -757,7 +757,7 @@ namespace Barotrauma return true; } - private static void CreateEditMenu(ValueNode? node, NodeConnection? connection = null) + private static void CreateEditMenu(ValueNode? node, EventEditorNodeConnection? connection = null) { object? newValue; Type? type; @@ -972,7 +972,7 @@ namespace Barotrauma { if (PlayerInput.PrimaryMouseButtonDown()) { - NodeConnection? connection = node.GetConnectionOnMouse(mousePos); + EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos); if (connection != null && connection.Type.NodeSide == NodeConnectionType.Side.Right) { if (connection.Type != NodeConnectionType.Out) @@ -1019,7 +1019,7 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked()) { - NodeConnection? connection = node.GetConnectionOnMouse(mousePos); + EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos); if (node.GetDrawRectangle().Contains(mousePos) || connection != null) { CreateContextMenu(node, node.GetConnectionOnMouse(mousePos)); @@ -1088,7 +1088,7 @@ namespace Barotrauma if (!DraggedConnection.CanConnect(nodeOnMouse)) { continue; } nodeOnMouse.ClearConnections(); - DraggedConnection.Parent.Connect(DraggedConnection, nodeOnMouse); + EditorNode.Connect(DraggedConnection, nodeOnMouse); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 072a2a6d9..917bf10f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -392,7 +392,7 @@ namespace Barotrauma Level.Loaded?.DrawDebugOverlay(spriteBatch, cam); if (GameMain.DebugDraw) { - MapEntity.mapEntityList.ForEach(me => me.AiTarget?.Draw(spriteBatch)); + MapEntity.MapEntityList.ForEach(me => me.AiTarget?.Draw(spriteBatch)); Character.CharacterList.ForEach(c => c.AiTarget?.Draw(spriteBatch)); if (GameMain.GameSession?.EventManager != null) { @@ -415,7 +415,7 @@ namespace Barotrauma GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); Color losColor; - if (GameMain.LightManager.LosMode is LosMode.Transparent or LosMode.BlockOutsideView) + if (GameMain.LightManager.LosMode == LosMode.Transparent) { //convert the los color to HLS and make sure the luminance of the color is always the same //as the luminance of the ambient light color diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 24e67f496..9d122c629 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -236,6 +236,7 @@ namespace Barotrauma } }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.test")) { @@ -314,6 +315,52 @@ namespace Barotrauma { RelativeOffset = new Vector2(leftPanel.RectTransform.RelativeSize.X * 2, 0.0f) }, style: "GUIFrameTop"); } + public void TestLevelGenerationForErrors(int amountOfLevelsToGenerate) + { + CoroutineManager.StartCoroutine(GenerateLevels()); + + IEnumerable GenerateLevels() + { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + for (int i = 0; i < amountOfLevelsToGenerate; i++) + { + Submarine.Unload(); + GameMain.LightManager.ClearLights(); + + currentLevelData = LevelData.CreateRandom(ToolBox.RandomSeed(10), generationParams: selectedParams); + currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; + var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); + DebugConsole.NewMessage("*****************************************************************************"); + DebugConsole.NewMessage($"Generating level {(i + 1)}/{amountOfLevelsToGenerate}: "); + DebugConsole.NewMessage(" Seed: " + currentLevelData.Seed); + DebugConsole.NewMessage(" Outpost parameters: " + (currentLevelData.ForceOutpostGenerationParams?.Name ?? "None")); + DebugConsole.NewMessage(" Level generation params: " + selectedParams.Identifier); + DebugConsole.NewMessage(" Mirrored: " + mirrorLevel.Selected); + DebugConsole.NewMessage(" Adjacent locations: " + (dummyLocations[0]?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (dummyLocations[1]?.Type.Identifier ?? "none".ToIdentifier())); + + yield return CoroutineStatus.Running; + + Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); + Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); + GameMain.LightManager.AddLight(pointerLightSource); + seedBox.Deselect(); + + if (errorCatcher.Errors.Any()) + { + DebugConsole.ThrowError("Error while generating level:"); + errorCatcher.Errors.ToList().ForEach(e => DebugConsole.ThrowError(e.Text)); + yield return CoroutineStatus.Success; + } + yield return CoroutineStatus.Running; + } + } + + + } + + + public override void Select() { base.Select(); @@ -903,6 +950,7 @@ namespace Barotrauma spriteBatch.End(); } + public override void Update(double deltaTime) { if (lightingEnabled.Selected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 959881cc8..bd977496b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -83,7 +83,6 @@ namespace Barotrauma { SetMenuTabPositioning(); CreateHostServerFields(); - CreateCampaignSetupUI(); SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); if (remoteContentDoc?.Root != null) { @@ -634,7 +633,7 @@ namespace Barotrauma campaignSetupUI.UpdateSubList(SubmarineInfo.SavedSubmarines); break; case Tab.LoadGame: - campaignSetupUI.UpdateLoadMenu(); + campaignSetupUI.CreateLoadMenu(); break; case Tab.Settings: SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); @@ -1224,7 +1223,7 @@ namespace Barotrauma var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); - campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame, SubmarineInfo.SavedSubmarines) + campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame) { LoadGame = LoadGame, StartNewGame = StartGame @@ -1550,6 +1549,14 @@ namespace Barotrauma 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 remote main menu content. " + + "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + + $"(error code: {remoteContentResponse.StatusCode})"); + return; + } string xml = remoteContentResponse.Content; int index = xml.IndexOf('<'); if (index > 0) { xml = xml.Substring(index, xml.Length - index); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 8536cf80a..b6169da32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -378,7 +378,7 @@ namespace Barotrauma } string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); - SaveUtil.DecompressToDirectory(path, dir, file => { }); + SaveUtil.DecompressToDirectory(path, dir); var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName)); if (!result.TryUnwrapSuccess(out var newPackage)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 886f4311f..f004779d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -22,8 +22,6 @@ namespace Barotrauma private GUIComponent jobVariantTooltip; - private SubmarinePreview submarinePreview; - private readonly GUITextBox chatInput; private readonly GUITextBox serverLogFilter; public GUITextBox ChatInput @@ -38,8 +36,10 @@ namespace Barotrauma private readonly GUIScrollBar levelDifficultyScrollBar; - private readonly GUIButton[] traitorProbabilityButtons; + private readonly List traitorElements = new List(); + private readonly GUIScrollBar traitorProbabilitySlider; private readonly GUITextBlock traitorProbabilityText; + private readonly GUILayoutGroup traitorDangerGroup; private readonly GUIButton[] botCountButtons; private readonly GUITextBlock botCountText; @@ -68,6 +68,7 @@ namespace Barotrauma public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; + public bool Spectating => spectateBox is { Selected: true, Visible: true }; private readonly GUIFrame playerInfoContainer; @@ -1070,15 +1071,27 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Settings"), font: GUIStyle.SubHeadingFont); - var settingsFrame = new GUIFrame(new RectTransform(Vector2.One, settingsHolder.RectTransform), style: "InnerFrame"); - var settingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsFrame.RectTransform, Anchor.Center)) + + var settingsFrameTop = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), settingsHolder.RectTransform), style: "InnerFrame"); + var settingsContentTop = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), settingsFrameTop.RectTransform, Anchor.Center)) { - RelativeSpacing = 0.025f + Stretch = true, + AbsoluteSpacing = GUI.IntScale(10) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 30) }, + TextManager.Get("TraitorSettings"), font: GUIStyle.SubHeadingFont); + + var settingsFrameBottom = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), settingsHolder.RectTransform), style: "InnerFrame"); + var settingsContentBottom = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.85f), settingsFrameBottom.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(10) }; //seed ------------------------------------------------------------------ - var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed")); + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), TextManager.Get("LevelSeed")); SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); SeedBox.OnDeselected += (textBox, key) => { @@ -1089,7 +1102,10 @@ namespace Barotrauma //level difficulty ------------------------------------------------------------------ - var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContent.RectTransform), style: null); + var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContentTop.RectTransform), style: null) + { + CanBeFocused = true + }; var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform), TextManager.Get("LevelDifficulty")) { @@ -1116,46 +1132,16 @@ namespace Barotrauma if (!EventManagerSettings.Prefabs.Any()) { return true; } difficultyName.Text = EventManagerSettings.GetByDifficultyPercentile(value).Name - + " (" + ((int)Math.Round(scrollbar.BarScrollValue)) + " %)"; + + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return true; }; clientDisabledElements.Add(levelDifficultyScrollBar); - //traitor probability ------------------------------------------------------------------ - - var traitorsSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsSettingHolder.RectTransform), TextManager.Get("Traitors"), wrap: true); - - var traitorProbContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorsSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - traitorProbabilityButtons = new GUIButton[2]; - traitorProbabilityButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: -1); - return true; - } - }; - - traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), - textAlignment: Alignment.Center, style: "GUITextBox"); - traitorProbabilityButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: 1); - return true; - } - }; - - clientDisabledElements.AddRange(traitorProbabilityButtons); - //bot count ------------------------------------------------------------------ - var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); var botCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; @@ -1178,10 +1164,10 @@ namespace Barotrauma return true; } }; - + botCountSettingHolder.RectTransform.MinSize = new Point(0, SeedBox.RectTransform.MinSize.Y); clientDisabledElements.AddRange(botCountButtons); - var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); var botSpawnModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; @@ -1205,23 +1191,108 @@ namespace Barotrauma } }; - List settingsElements = settingsContent.Children.ToList(); - for (int i = 0; i < settingsElements.Count; i++) - { - if (settingsElements[i].CountChildren > 0) - { - settingsElements[i].RectTransform.MinSize = new Point(0, Math.Max(settingsElements[i].RectTransform.Children.Max(c => c.Rect.Height), (int)(20 * GUI.Scale))); - } - } + clientDisabledElements.AddRange(botSpawnModeButtons); - settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrame.RectTransform), style: "InnerFrame") + settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrameTop.RectTransform), style: "InnerFrame") { - Color = Color.Black * 0.5f, + Color = Color.Black * 0.85f, IgnoreLayoutGroups = true, Visible = false }; + new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.3f), settingsBlocker.RectTransform, Anchor.Center), + TextManager.Get("settings.campaigndisabled"), wrap: true, style: "InnerFrame", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - clientDisabledElements.AddRange(botSpawnModeButtons); + //traitor probability ------------------------------------------------------------------ + + var traitorsProbHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), settingsContentBottom.RectTransform), style: null); + + var traitorProbLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform), TextManager.Get("traitor.probability")); + + traitorProbabilitySlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) + { + Step = 0.05f, + Range = new Vector2(0.0f, 1.0f), + ToolTip = TextManager.Get("traitor.probability.tooltip"), + OnReleased = (scrollbar, value) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorProbability: value); + return true; + } + }; + var traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), traitorProbLabel.RectTransform), "", textAlignment: Alignment.CenterRight) + { + ToolTip = TextManager.Get("traitor.probability.tooltip") + }; + traitorProbabilitySlider.OnMoved = (scrollbar, value) => + { + traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); + traitorProbabilityText.TextColor = + value <= 0.0f ? + GUIStyle.Green : + ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); + return true; + }; + + traitorElements.Clear(); + traitorElements.Add(traitorProbabilityText); + traitorElements.Add(traitorProbabilitySlider); + clientDisabledElements.Add(traitorProbabilitySlider); + + var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), settingsContentBottom.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) + { + ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") + }; + + var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var traitorDangerButtons = new GUIButton[2]; + traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); + return true; + } + }; + + traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 1 + }; + for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) + { + var difficultyColor = Mission.GetDifficultyColor(i); + new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) + { + ToolTip = + RichString.Rich( + $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + + TextManager.Get($"traitor.dangerlevel.{i}.description")), + Color = difficultyColor, + DisabledColor = Color.Gray * 0.5f, + }; + } + + traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); + return true; + } + }; + + SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); + traitorElements.AddRange(traitorDangerButtons); + clientDisabledElements.AddRange(traitorDangerButtons); + + settingsContentTop.Recalculate(); + settingsContentBottom.Recalculate(); } public void StopWaitingForStartRound() @@ -1264,6 +1335,7 @@ namespace Barotrauma public override void Deselect() { + SaveAppearance(); chatInput.Deselect(); CampaignCharacterDiscarded = false; @@ -1338,34 +1410,35 @@ namespace Barotrauma public void RefreshEnabledElements() { - ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - missionTypeList.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + bool manageSettings = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + + ServerName.Readonly = !manageSettings; + ServerMessage.Readonly = !manageSettings; + missionTypeList.Enabled = manageSettings; foreach (var tickBox in missionTypeTickBoxes) { - tickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + tickBox.Enabled = manageSettings; } - SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; + levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; levelDifficultyScrollBar.ToolTip = string.Empty; if (!levelDifficultyScrollBar.Enabled) { levelDifficultyScrollBar.ToolTip = TextManager.Get("campaigndifficultydisabled"); } - traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = - !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + traitorElements.ForEach(e => e.Enabled = manageSettings); + botCountButtons[0].Enabled = botCountButtons[1].Enabled = manageSettings; + botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = manageSettings; - autoRestartBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + autoRestartBox.Enabled = manageSettings; - SettingsButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SettingsButton.Visible = manageSettings; SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; - ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings) && !GameMain.Client.GameStarted; + ServerName.Readonly = !manageSettings; + ServerMessage.Readonly = !manageSettings; + shuttleTickBox.Enabled = manageSettings && !GameMain.Client.GameStarted; SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub) && !GameMain.Client.GameStarted; ModeList.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.ServerSettings.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode)); @@ -1375,7 +1448,7 @@ namespace Barotrauma roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); - SubVisibilityButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SubVisibilityButton.Visible = manageSettings; ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; @@ -1415,6 +1488,7 @@ namespace Barotrauma public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { + if (GameMain.Client == null) { return; } UpdatePlayerFrame( Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, @@ -1424,6 +1498,7 @@ namespace Barotrauma private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true) { + if (GameMain.Client == null) { return; } createPendingChangesText = createPendingText; if (characterInfo == null || CampaignCharacterDiscarded) { @@ -1447,7 +1522,6 @@ namespace Barotrauma bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; changesPendingText = null; - if (TabMenu.PendingChanges) { CreateChangesPendingText(); @@ -1754,6 +1828,16 @@ namespace Barotrauma jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); } + private void SetTraitorDangerIndicators(int dangerLevel) + { + int i = 0; + foreach (var child in traitorDangerGroup.Children) + { + child.Enabled = i < dangerLevel; + i++; + } + } + public bool ToggleSpectate(GUITickBox tickBox) { SetSpectate(tickBox.Selected); @@ -2088,7 +2172,7 @@ namespace Barotrauma private Action DrawDownloadThrobber(Client client, params GUIComponent[] otherComponents) => (sb, c) => DrawDownloadThrobber(client, otherComponents, sb, c); //poor man's currying - private void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component) + private static void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component) { if (!client.IsDownloading) { @@ -2322,7 +2406,14 @@ namespace Barotrauma PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } + OnClicked = (btn, userdata) => + { + if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) + { + ClosePlayerFrame(btn, userdata); + } + return true; + } }; new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PlayerFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); @@ -2412,7 +2503,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2432,7 +2523,7 @@ namespace Barotrauma foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { - if (permission == ClientPermissions.None || permission == ClientPermissions.All) continue; + if (permission == ClientPermissions.None || permission == ClientPermissions.All) { continue; } var permissionTick = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), permissionsBox.Content.RectTransform), TextManager.Get("ClientPermission." + permission), font: GUIStyle.SmallFont) @@ -2445,7 +2536,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } var thisPermission = (ClientPermissions)tickBox.UserData; if (tickBox.Selected) @@ -2480,7 +2571,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2803,7 +2894,7 @@ namespace Barotrauma publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } - private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) + private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) { var itemIdentifiers = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant] .Where(it => it.ShowPreview) @@ -2938,28 +3029,22 @@ namespace Barotrauma { OnHeadSwitch = menu => { - StoreHead(true); UpdateJobPreferences(info); SelectAppearanceTab(button, _); - }, - OnSliderMoved = (bar, scroll) => - { - StoreHead(false); - return false; - }, - OnSliderReleased = SaveHead + } }; return false; } - private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); - private bool StoreHead(bool save) + public bool SaveAppearance() { - var info = GameMain.Client.CharacterInfo; + var info = GameMain.Client?.CharacterInfo; + if (info?.Head == null) { return false; } var characterConfig = MultiplayerPreferences.Instance; - characterConfig.TagSet.Clear(); characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); + characterConfig.TagSet.Clear(); + characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); characterConfig.HairIndex = info.Head.HairIndex; characterConfig.BeardIndex = info.Head.BeardIndex; characterConfig.MoustacheIndex = info.Head.MoustacheIndex; @@ -2968,15 +3053,12 @@ namespace Barotrauma characterConfig.FacialHairColor = info.Head.FacialHairColor; characterConfig.SkinColor = info.Head.SkinColor; - if (save) + if (GameMain.GameSession?.IsRunning ?? false) { - if (GameMain.GameSession?.IsRunning ?? false) - { - TabMenu.PendingChanges = true; - CreateChangesPendingText(); - } - GameSettings.SaveCurrentConfig(); + TabMenu.PendingChanges = true; + CreateChangesPendingText(); } + GameSettings.SaveCurrentConfig(); return true; } @@ -3143,7 +3225,7 @@ namespace Barotrauma return true; } - private GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) + private static GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) { GUIFrame innerFrame = null; List outfitPreviews = jobPrefab.GetJobOutfitSprites(CharacterPrefab.HumanPrefab.CharacterInfoPrefab, useInventoryIcon: true, out var maxDimensions); @@ -3221,6 +3303,7 @@ namespace Barotrauma if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) { + SaveAppearance(); UpdatePlayerFrame(null); GameMain.Client.ConnectedClients.ForEach(c => SetPlayerNameAndJobPreference(c)); } @@ -3363,7 +3446,11 @@ namespace Barotrauma if (!enabled) { //remove campaign character from the panel - if (campaignCharacterInfo != null) { UpdatePlayerFrame(null); } + if (campaignCharacterInfo != null) + { + UpdatePlayerFrame(null); + SetSpectate(spectateBox.Selected); + } campaignCharacterInfo = null; CampaignCharacterDiscarded = false; } @@ -3411,7 +3498,7 @@ namespace Barotrauma private bool ViewJobInfo(GUIButton button, object obj) { - if (!(button.UserData is JobVariant jobPrefab)) { return false; } + if (button.UserData is not JobVariant jobPrefab) { return false; } JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(out GUIComponent buttonContainer); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), @@ -3541,7 +3628,7 @@ namespace Barotrauma } } - private GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) + private static GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { float relativeSize = 0.15f; @@ -3631,9 +3718,13 @@ namespace Barotrauma } if (subList == SubList) + { FailedSelectedSub = null; + } else + { FailedSelectedShuttle = null; + } //hashes match, all good if (sub.MD5Hash?.StringRepresentation == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index a47aa5922..d9a3de1c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1047,7 +1047,7 @@ namespace Barotrauma } if (filterTraitorValue != TernaryOption.Any) { - if ((serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) != (filterTraitorValue == TernaryOption.Enabled)) + if ((serverInfo.TraitorProbability > 0.0f) != (filterTraitorValue == TernaryOption.Enabled)) { return false; } @@ -1829,8 +1829,6 @@ namespace Barotrauma { graphics.Clear(Color.CornflowerBlue); - GameMain.TitleScreen.DrawLoadingText = false; - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a9b03c0ec..bbae3c7c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -75,6 +75,7 @@ namespace Barotrauma NoCargoSpawnpoints, NoBallastTag, NonLinkedGaps, + NoHiddenContainers, StructureCount, WallCount, ItemCount, @@ -232,6 +233,8 @@ namespace Barotrauma public override Camera Cam => cam; + public bool DrawCharacterInventory => dummyCharacter != null && WiringMode; + public static XDocument AutoSaveInfo; private static readonly string autoSavePath = Path.Combine("Submarines", ".AutoSaves"); private static readonly string autoSaveInfoPath = Path.Combine(autoSavePath, "autosaves.xml"); @@ -583,7 +586,7 @@ namespace Barotrauma if (!(o is string layer)) { return false; } MapEntity.SelectedList.Clear(); - foreach (MapEntity entity in MapEntity.mapEntityList.Where(me => !me.Removed && me.Layer == layer)) + foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer)) { if (entity.IsSelected) { continue; } @@ -842,7 +845,7 @@ namespace Barotrauma var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => { - int count = MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count; + int count = MapEntity.MapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count; structureCount.TextColor = count > MaxStructures ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxStructures); return count.ToString(); }; @@ -1163,7 +1166,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in entityLists[categoryKey]) { #if !DEBUG - if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } + if ((ep.HideInMenus || ep.HideInEditors) && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } @@ -1182,7 +1185,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } + if ((ep.HideInMenus || ep.HideInEditors) && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } @@ -1220,7 +1223,7 @@ namespace Barotrauma frame.Color = Color.Magenta; frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"; } - if (ep.HideInMenus) + if (ep.HideInMenus || ep.HideInEditors) { frame.Color = Color.Red; name = "[HIDDEN] " + name; @@ -1638,7 +1641,7 @@ namespace Barotrauma /// The saving is ran in another thread to avoid lag spikes private static void AutoSave() { - if (MapEntity.mapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving) + if (MapEntity.MapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving) { if (MainSub != null) { @@ -1939,7 +1942,7 @@ namespace Barotrauma var matchingFile = modProject.Files.FirstOrDefault(f => f.Type == subFileType && filePath.CleanUpPath().Equals(f.Path.CleanUpPath(), StringComparison.OrdinalIgnoreCase)); if (matchingFile != null) { - File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir)); + File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir, StringComparison.OrdinalIgnoreCase)); modProject.RemoveFile(matchingFile); } var newFile = ModProject.File.FromPath(filePath, subFileType); @@ -2852,7 +2855,7 @@ namespace Barotrauma { OnClicked = (button, o) => { - var requiredPackages = MapEntity.mapEntityList.Select(e => e?.Prefab?.ContentPackage) + var requiredPackages = MapEntity.MapEntityList.Select(e => e?.Prefab?.ContentPackage) .Where(cp => cp != null) .Distinct().OfType().Select(p => p.Name).ToHashSet(); var tickboxes = requiredContentPackList.Content.Children.OfType().ToArray(); @@ -3493,7 +3496,15 @@ namespace Barotrauma var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); if (ownerPackage is null) { - if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) + if (IsVanillaSub(selectedSubInfo)) + { +#if DEBUG + LoadSub(selectedSubInfo); +#else + AskLoadVanillaSub(selectedSubInfo); +#endif + } + else if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) { if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId) && publishedWorkshopItemIds.Contains(workshopId.Value)) @@ -3505,14 +3516,6 @@ namespace Barotrauma AskLoadSubscribedSub(selectedSubInfo); } } - else if (IsVanillaSub(selectedSubInfo)) - { -#if DEBUG - LoadSub(selectedSubInfo); -#else - AskLoadVanillaSub(selectedSubInfo); -#endif - } } else { @@ -3792,10 +3795,7 @@ namespace Barotrauma wiringModeTickBox.Selected = newMode == Mode.Wiring; lockMode = false; - foreach (MapEntity me in MapEntity.mapEntityList) - { - me.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); @@ -3806,7 +3806,8 @@ namespace Barotrauma { var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null); dummyCharacter.Inventory.TryPutItem(item, null, new List() { InvSlotType.RightHand }); - wiringToolPanel = CreateWiringPanel(); + Point wirePos = new Point((int)(10 * GUI.Scale), TopPanel.Rect.Height + entityCountPanel.Rect.Height + (int)(10 * GUI.Scale)); + wiringToolPanel = CreateWiringPanel(wirePos, SelectWire); } } @@ -3830,11 +3831,11 @@ namespace Barotrauma Item target = null; var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && ic is not Repairable && ic.GuiFrame != null)) + if (single is Item item && item.Components.Any(static ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { // Do not offer the ability to open the inventory if the inventory should never be drawn - var container = item.GetComponent(); - if (container == null || container.DrawInventory) { target = item; } + var containers = item.GetComponents(); + if (containers.Any(static c => c.DrawInventory) || item.GetComponent() is not null) { target = item; } } bool hasTargets = targets.Count > 0; @@ -3850,7 +3851,7 @@ namespace Barotrauma new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate { bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null); - foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) + foreach (MapEntity match in MapEntity.MapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) { if (MapEntity.SelectedList.Contains(match)) { continue; } if (match is Gap gap) @@ -3892,7 +3893,7 @@ namespace Barotrauma new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => { - foreach (MapEntity match in MapEntity.mapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e)))) + foreach (MapEntity match in MapEntity.MapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e)))) { if (MapEntity.SelectedList.Contains(match)) { continue; } MapEntity.SelectedList.Add(match); @@ -3965,7 +3966,7 @@ namespace Barotrauma { Layers.Remove(original); - foreach (MapEntity entity in MapEntity.mapEntityList.Where(entity => entity.Layer == original)) + foreach (MapEntity entity in MapEntity.MapEntityList.Where(entity => entity.Layer == original)) { entity.Layer = newName ?? string.Empty; } @@ -3980,7 +3981,7 @@ namespace Barotrauma private void ReconstructLayers() { ClearLayers(); - foreach (MapEntity entity in MapEntity.mapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { if (!string.IsNullOrWhiteSpace(entity.Layer)) { @@ -4307,15 +4308,15 @@ namespace Barotrauma static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}"; } - private GUIFrame CreateWiringPanel() + public static GUIFrame CreateWiringPanel(Point offset, GUIListBox.OnSelectedHandler onWireSelected) { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas) - { MinSize = new Point(120, 300), AbsoluteOffset = new Point((int)(10 * GUI.Scale), TopPanel.Rect.Height + entityCountPanel.Rect.Height + (int)(10 * GUI.Scale)) }); + { MinSize = new Point(120, 300), AbsoluteOffset = offset }); GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { PlaySoundOnSelect = true, - OnSelected = SelectWire, + OnSelected = onWireSelected, CanTakeKeyBoardFocus = false }; @@ -4323,12 +4324,14 @@ namespace Barotrauma foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus) { continue; } - if (!itemPrefab.Tags.Contains("wire")) { continue; } + if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus || itemPrefab.HideInEditors) { continue; } + if (!itemPrefab.Tags.Contains(Tags.WireItem)) { continue; } + if (CircuitBox.IsInGame() && itemPrefab.Tags.Contains(Tags.Thalamus)) { continue; } + wirePrefabs.Add(itemPrefab); } - foreach (ItemPrefab itemPrefab in wirePrefabs.OrderBy(w => !w.CanBeBought).ThenBy(w => w.UintIdentifier)) + foreach (ItemPrefab itemPrefab in wirePrefabs.OrderBy(static w => !w.CanBeBought).ThenBy(static w => w.UintIdentifier)) { GUIFrame imgFrame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, listBox.Rect.Width / 2), listBox.Content.RectTransform), style: "ListBoxElement") { @@ -4393,7 +4396,7 @@ namespace Barotrauma { if (dummyCharacter == null || itemContainer == null) { return; } - if (((itemContainer.GetComponent() is { } holdable && !holdable.Attached) || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) + if ((itemContainer.GetComponent() is { Attached: false } || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) { // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it oldItemPosition = itemContainer.SimPosition; @@ -4614,9 +4617,9 @@ namespace Barotrauma public void AutoHull() { - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + for (int i = 0; i < MapEntity.MapEntityList.Count; i++) { - MapEntity h = MapEntity.mapEntityList[i]; + MapEntity h = MapEntity.MapEntityList[i]; if (h is Hull || h is Gap) { h.Remove(); @@ -4629,7 +4632,7 @@ namespace Barotrauma List mapEntityList = new List(); - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.MapEntityList) { if (e is Item it) { @@ -5086,10 +5089,12 @@ namespace Barotrauma layerGroup.Recalculate(); - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft); + if (textBlock.TextSize.X > textBlock.Rect.Width) { - CanBeFocused = false - }; + textBlock.ToolTip = textBlock.Text; + textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + } layerGroup.Recalculate(); layerChainLayout.Recalculate(); @@ -5212,7 +5217,7 @@ namespace Barotrauma var highlightedEntities = new List(); // ReSharper disable once LoopCanBeConvertedToQuery - foreach (Item item in MapEntity.mapEntityList.Where(entity => entity is Item).Cast()) + foreach (Item item in MapEntity.MapEntityList.Where(entity => entity is Item).Cast()) { var wire = item.GetComponent(); if (wire == null || !wire.IsMouseOn()) { continue; } @@ -5225,8 +5230,9 @@ namespace Barotrauma hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); - saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode; - snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode; + bool isCircuitBoxOpened = dummyCharacter?.SelectedItem?.GetComponent() is not null; + saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode && !isCircuitBoxOpened; + snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode && !isCircuitBoxOpened; var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; @@ -5285,7 +5291,7 @@ namespace Barotrauma { if (wiringToolPanel.GetChild() is { } listBox) { - if (!dummyCharacter.HeldItems.Any(it => it.HasTag("wire"))) + if (!dummyCharacter.HeldItems.Any(it => it.HasTag(Tags.WireItem))) { listBox.Deselect(); } @@ -5394,7 +5400,7 @@ namespace Barotrauma } else { - var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + var selectables = MapEntity.MapEntityList.Where(entity => entity.SelectableInEditor).ToList(); foreach (var item in Item.ItemList) { //attached wires are not normally selectable (by clicking), @@ -5418,7 +5424,7 @@ namespace Barotrauma } else { - cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); + cam.MoveCamera((float) deltaTime, allowMove: !CircuitBox.IsCircuitBoxSelected(dummyCharacter), allowZoom: GUI.MouseOn == null); } } else @@ -5426,7 +5432,7 @@ namespace Barotrauma cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); } - if (PlayerInput.MidButtonHeld()) + if (PlayerInput.MidButtonHeld() && !CircuitBox.IsCircuitBoxSelected(dummyCharacter)) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / cam.Zoom; moveSpeed.X = -moveSpeed.X; @@ -5460,10 +5466,7 @@ namespace Barotrauma { if (WiringMode) { - foreach (MapEntity me in MapEntity.mapEntityList) - { - me.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); if (dummyCharacter.SelectedItem == null) { @@ -5948,13 +5951,10 @@ namespace Barotrauma } } - if (dummyCharacter != null) + if (DrawCharacterInventory) { - if (WiringMode) - { - dummyCharacter.DrawHUD(spriteBatch, cam, false); - wiringToolPanel.DrawManually(spriteBatch); - } + dummyCharacter.DrawHUD(spriteBatch, cam, false); + wiringToolPanel.DrawManually(spriteBatch); } MapEntity.DrawEditor(spriteBatch, cam); @@ -5991,10 +5991,7 @@ namespace Barotrauma private void CreateImage(int width, int height, System.IO.Stream stream) { MapEntity.SelectedList.Clear(); - foreach (MapEntity me in MapEntity.mapEntityList) - { - me.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; @@ -6037,16 +6034,21 @@ namespace Barotrauma private void DrawGrid(SpriteBatch spriteBatch) { // don't render at high zoom levels because it would just turn the screen white - if (cam.Zoom < 0.5f || !ShouldDrawGrid) { return; } + if (!ShouldDrawGrid) { return; } var (gridX, gridY) = Submarine.GridSize; + DrawGrid(spriteBatch, cam, gridX, gridY, zoomTreshold: true); + } + public static void DrawGrid(SpriteBatch spriteBatch, Camera cam, float sizeX, float sizeY, bool zoomTreshold) + { + if (zoomTreshold && cam.Zoom < 0.5f) { return; } int scale = Math.Max(1, GUI.IntScale(1)); float zoom = cam.Zoom / 2f; // Don't ask float lineThickness = Math.Max(1, scale / zoom); Color gridColor = gridBaseColor; - if (cam.Zoom < 1.0f) + if (zoomTreshold && cam.Zoom < 1.0f) { // fade the grid when zooming out gridColor *= Math.Max(0, (cam.Zoom - 0.5f) * 2f); @@ -6054,18 +6056,66 @@ namespace Barotrauma Rectangle camRect = cam.WorldView; - for (float x = snapX(camRect.X); x < snapX(camRect.X + camRect.Width) + gridX; x += gridX) + for (float x = snapX(camRect.X); x < snapX(camRect.X + camRect.Width) + sizeX; x += sizeX) { spriteBatch.DrawLine(new Vector2(x, -camRect.Y), new Vector2(x, -(camRect.Y - camRect.Height)), gridColor, thickness: lineThickness); } - for (float y = snapY(camRect.Y); y >= snapY(camRect.Y - camRect.Height) - gridY; y -= Submarine.GridSize.Y) + for (float y = snapY(camRect.Y); y >= snapY(camRect.Y - camRect.Height) - sizeY; y -= sizeY) { spriteBatch.DrawLine(new Vector2(camRect.X, -y), new Vector2(camRect.Right, -y), gridColor, thickness: lineThickness); } - float snapX(int x) => (float) Math.Floor(x / gridX) * gridX; - float snapY(int y) => (float) Math.Ceiling(y / gridY) * gridY; + float snapX(int x) => (float) Math.Floor(x / sizeX) * sizeX; + float snapY(int y) => (float) Math.Ceiling(y / sizeY) * sizeY; + } + + public static void DrawOutOfBoundsArea(SpriteBatch spriteBatch, Camera cam, float playableAreaSize, Color color) + { + Rectangle camRect = cam.WorldView; + + RectangleF playableArea = new RectangleF( + -playableAreaSize / 2f, + -playableAreaSize / 2f, + playableAreaSize, + playableAreaSize + ); + + RectangleF topRect = new( + camRect.Left, + -camRect.Top, + camRect.Width, + playableArea.Top + camRect.Top + ); + + // idk why camRect.Bottom doesn't work here + float camRectBottom = -camRect.Top + camRect.Height; + + RectangleF bottomRect = new( + camRect.Left, + playableArea.Bottom, + camRect.Width, + camRectBottom + playableArea.Bottom + ); + + RectangleF rightRect = new( + playableArea.Right, + playableArea.Top, + camRect.Right - playableArea.Right, + playableArea.Height + ); + + RectangleF leftRect = new( + playableArea.Left, + playableArea.Top, + camRect.Left - playableArea.Left, + playableArea.Height + ); + + GUI.DrawFilledRectangle(spriteBatch, topRect, color); + GUI.DrawFilledRectangle(spriteBatch, leftRect, color); + GUI.DrawFilledRectangle(spriteBatch, rightRect, color); + GUI.DrawFilledRectangle(spriteBatch, bottomRect, color); } public void SaveScreenShot(int width, int height, string filePath) @@ -6116,7 +6166,7 @@ namespace Barotrauma public static ImmutableHashSet GetEntitiesInSameLayer(MapEntity entity) { if (string.IsNullOrWhiteSpace(entity.Layer)) { return ImmutableHashSet.Empty; } - return MapEntity.mapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet(); + return MapEntity.MapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 7cf2b716b..115402131 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -45,7 +45,7 @@ namespace Barotrauma base.Select(); if (dummyCharacter is { Removed: false }) { - dummyCharacter?.Remove(); + dummyCharacter.Remove(); } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); @@ -54,15 +54,11 @@ namespace Barotrauma dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Info.GiveExperience(999999); - miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false); + miniMapItem = new Item(ItemPrefab.Find(null, "circuitbox".ToIdentifier()), Vector2.Zero, null, 1337, false); + miniMapItem.GetComponent().AttachToWall(); - foreach (ItemComponent component in miniMapItem.Components) - { - component.OnItemLoaded(); - } Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); - TabMenu = new TabMenu(); } public override void AddToGUIUpdateList() @@ -78,29 +74,30 @@ namespace Barotrauma base.Update(deltaTime); TabMenu?.Update((float)deltaTime); - // if (dummyCharacter is { } dummy && miniMapItem is { } item) - // { - // if (dummy.SelectedConstruction != item) - // { - // dummy.SelectedConstruction = item; - // } - // - // dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime); - // Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); - // - // foreach (Limb limb in dummy.AnimController.Limbs) - // { - // limb.body.SetTransform(pos, 0.0f); - // } - // - // if (dummy.AnimController?.Collider is { } collider) - // { - // collider.SetTransform(pos, 0); - // } - // - // dummy.ControlLocalPlayer((float)deltaTime, Cam, false); - // dummy.Control((float)deltaTime, Cam); - // } + if (dummyCharacter is { } dummy && miniMapItem is { } item) + { + if (dummy.SelectedItem != item) + { + dummy.SelectedItem = item; + } + + dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime); + item.SendSignal("1", "signal_in1"); + Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); + + foreach (Limb limb in dummy.AnimController.Limbs) + { + limb.body.SetTransform(pos, 0.0f); + } + + if (dummy.AnimController?.Collider is { } collider) + { + collider.SetTransform(pos, 0); + } + + dummy.ControlLocalPlayer((float)deltaTime, Cam, false); + dummy.Control((float)deltaTime, Cam); + } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 44ba1c654..8cd9300f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -390,7 +390,7 @@ namespace Barotrauma } if (toolTip.IsNullOrEmpty()) { - toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); + toolTip = TextManager.Get($"{propertyTag}.description", $"{fallbackTag}.description", $"sp.{fallbackTag}.description"); } if (toolTip.IsNullOrEmpty()) { @@ -1334,9 +1334,11 @@ namespace Barotrauma } } } - + private static void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) { + if (IsEntityRemoved(entity)) { return; } + if (GameMain.Client != null) { if (entity is Item item) @@ -1352,7 +1354,7 @@ namespace Barotrauma private bool SetPropertyValue(SerializableProperty property, object entity, object value) { - if (LockEditing) { return false; } + if (LockEditing || IsEntityRemoved(entity)) { return false; } object oldData = property.GetValue(entity); // some properties have null as the default string value @@ -1403,6 +1405,9 @@ namespace Barotrauma return property.TrySetValue(entity, value); } + public static bool IsEntityRemoved(object entity) + => entity is Entity { Removed: true } or ItemComponent { Item.Removed: true }; + public static void CommitCommandBuffer() { if (CommandBuffer != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 3214a3ff5..4d443268b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -734,10 +734,16 @@ namespace Barotrauma { OnSelected = tickBox => { + GUIMessageBox? loadingBox = null; + if (!tickBox.Selected) + { + loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); + } GameAnalyticsManager.SetConsent( tickBox.Selected ? GameAnalyticsManager.Consent.Ask - : GameAnalyticsManager.Consent.No); + : GameAnalyticsManager.Consent.No, + onAnswerSent: () => loadingBox?.Close()); return false; } }; @@ -766,7 +772,7 @@ namespace Barotrauma statisticsTickBox.OnSelected = null; statisticsTickBox.Selected = shouldTickBoxBeSelected; statisticsTickBox.OnSelected = prevHandler; - statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; + statisticsTickBox.Enabled &= GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; }); #endif } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 1f1dca9e1..f816a71a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -2,21 +2,89 @@ using OpenAL; using NVorbis; using System.Collections.Generic; +using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma.Sounds { - class OggSound : Sound + sealed class OggSound : Sound { - private VorbisReader reader; + private VorbisReader streamReader; - //key = sample rate, value = filter - private static readonly Dictionary muffleFilters = new Dictionary(); - - private static List playbackAmplitude; + private List playbackAmplitude; private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file - public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, stream, true, xElement) { } + private short[] sampleBuffer = Array.Empty(); + private short[] muffleBuffer = Array.Empty(); + public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, + stream, true, xElement) + { + var reader = new VorbisReader(Filename); + + ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; + SampleRate = reader.SampleRate; + + if (stream) + { + streamReader = reader; + return; + } + + TaskPool.Add( + $"LoadSamples {filename}", + LoadSamples(reader), + t => + { + reader.Dispose(); + if (!t.TryGetResult(out TaskResult result)) + { + return; + } + sampleBuffer = result.SampleBuffer; + muffleBuffer = result.MuffleBuffer; + playbackAmplitude = result.PlaybackAmplitude; + Owner.KillChannels(this); // prevents INVALID_OPERATION error + buffers?.Dispose(); buffers = null; + }); + } + + private readonly record struct TaskResult( + short[] SampleBuffer, + short[] MuffleBuffer, + List PlaybackAmplitude); + + private static async Task LoadSamples(VorbisReader reader) + { + reader.DecodedPosition = 0; + + int bufferSize = (int)reader.TotalSamples * reader.Channels; + + float[] floatBuffer = new float[bufferSize]; + var sampleBuffer = new short[bufferSize]; + var muffledBuffer = new short[bufferSize]; + + int readSamples = await Task.Run(() => reader.ReadSamples(floatBuffer, 0, bufferSize)); + + var playbackAmplitude = new List(); + for (int i = 0; i < bufferSize; i += reader.Channels * AMPLITUDE_SAMPLE_COUNT) + { + float maxAmplitude = 0.0f; + for (int j = i; j < i + reader.Channels * AMPLITUDE_SAMPLE_COUNT; j++) + { + if (j >= bufferSize) { break; } + maxAmplitude = Math.Max(maxAmplitude, Math.Abs(floatBuffer[j])); + } + playbackAmplitude.Add(maxAmplitude); + } + + CastBuffer(floatBuffer, sampleBuffer, readSamples); + + MuffleBuffer(floatBuffer, reader.SampleRate); + + CastBuffer(floatBuffer, muffledBuffer, readSamples); + + return new TaskResult(sampleBuffer, muffledBuffer, playbackAmplitude); + } public override float GetAmplitudeAtPlaybackPos(int playbackPos) { @@ -27,101 +95,65 @@ namespace Barotrauma.Sounds return playbackAmplitude[index]; } + private float[] streamFloatBuffer = null; public override int FillStreamBuffer(int samplePos, short[] buffer) { if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); } - if (reader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } + if (streamReader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } - if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0; + if (samplePos >= streamReader.TotalSamples * streamReader.Channels * 2) return 0; - samplePos /= reader.Channels * 2; - reader.DecodedPosition = samplePos; + samplePos /= streamReader.Channels * 2; + streamReader.DecodedPosition = samplePos; - float[] floatBuffer = new float[buffer.Length]; - int readSamples = reader.ReadSamples(floatBuffer, 0, buffer.Length / 2); + if (streamFloatBuffer is null || streamFloatBuffer.Length < buffer.Length) + { + streamFloatBuffer = new float[buffer.Length]; + } + int readSamples = streamReader.ReadSamples(streamFloatBuffer, 0, buffer.Length); //MuffleBuffer(floatBuffer, reader.Channels); - CastBuffer(floatBuffer, buffer, readSamples); + CastBuffer(streamFloatBuffer, buffer, readSamples); return readSamples; } - static void MuffleBuffer(float[] buffer, int sampleRate, int channelCount) + static void MuffleBuffer(float[] buffer, int sampleRate) { - if (!muffleFilters.TryGetValue(sampleRate, out BiQuad filter)) - { - filter = new LowpassFilter(sampleRate, 1600); - muffleFilters.Add(sampleRate, filter); - } + var filter = new LowpassFilter(sampleRate, 1600); filter.Process(buffer); } - public override void InitializeALBuffers() + public override void InitializeAlBuffers() { - base.InitializeALBuffers(); - - reader ??= new VorbisReader(Filename); - - ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; - SampleRate = reader.SampleRate; - - if (Buffers != null && SoundBuffers.BuffersGenerated < SoundBuffers.MaxBuffers) + if (buffers != null && SoundBuffers.BuffersGenerated < SoundBuffers.MaxBuffers) { - Buffers.RequestAlBuffers(); FillBuffers(); + FillAlBuffers(); } } - public override void FillBuffers() + public override void FillAlBuffers() { - if (!Stream) + if (Stream) { return; } + if (sampleBuffer.Length == 0 || muffleBuffer.Length == 0) { return; } + buffers ??= new SoundBuffers(this); + if (!buffers.RequestAlBuffers()) { return; } + + Al.BufferData(buffers.AlBuffer, ALFormat, sampleBuffer, + sampleBuffer.Length * sizeof(short), SampleRate); + + int alError = Al.GetError(); + if (alError != Al.NoError) { - reader ??= new VorbisReader(Filename); + throw new Exception("Failed to set regular buffer data for non-streamed audio! " + Al.GetErrorString(alError)); + } - reader.DecodedPosition = 0; + Al.BufferData(buffers.AlMuffledBuffer, ALFormat, muffleBuffer, + muffleBuffer.Length * sizeof(short), SampleRate); - int bufferSize = (int)reader.TotalSamples * reader.Channels; - - float[] floatBuffer = new float[bufferSize]; - short[] shortBuffer = new short[bufferSize]; - - int readSamples = reader.ReadSamples(floatBuffer, 0, bufferSize); - - playbackAmplitude = new List(); - for (int i = 0; i < bufferSize; i += reader.Channels * AMPLITUDE_SAMPLE_COUNT) - { - float maxAmplitude = 0.0f; - for (int j = i; j < i + reader.Channels * AMPLITUDE_SAMPLE_COUNT; j++) - { - if (j >= bufferSize) { break; } - maxAmplitude = Math.Max(maxAmplitude, Math.Abs(floatBuffer[j])); - } - playbackAmplitude.Add(maxAmplitude); - } - - CastBuffer(floatBuffer, shortBuffer, readSamples); - - Al.BufferData(Buffers.AlBuffer, ALFormat, shortBuffer, - readSamples * sizeof(short), SampleRate); - - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set regular buffer data for non-streamed audio! " + Al.GetErrorString(alError)); - } - - MuffleBuffer(floatBuffer, SampleRate, reader.Channels); - - CastBuffer(floatBuffer, shortBuffer, readSamples); - - Al.BufferData(Buffers.AlMuffledBuffer, ALFormat, shortBuffer, - readSamples * sizeof(short), SampleRate); - - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set muffled buffer data for non-streamed audio! " + Al.GetErrorString(alError)); - } - - reader.Dispose(); reader = null; + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to set muffled buffer data for non-streamed audio! " + Al.GetErrorString(alError)); } } @@ -129,7 +161,7 @@ namespace Barotrauma.Sounds { if (Stream) { - reader?.Dispose(); + streamReader?.Dispose(); } base.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index d911d4ac2..8d2ea80c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -33,7 +33,7 @@ namespace Barotrauma.Sounds } } - private SoundBuffers buffers; + protected SoundBuffers buffers; public SoundBuffers Buffers { get { return !Stream ? buffers : null; } @@ -72,8 +72,6 @@ namespace Barotrauma.Sounds BaseGain = 1.0f; BaseNear = 100.0f; BaseFar = 200.0f; - - InitializeALBuffers(); } public override string ToString() @@ -141,21 +139,11 @@ namespace Barotrauma.Sounds public abstract float GetAmplitudeAtPlaybackPos(int playbackPos); - public virtual void InitializeALBuffers() - { - if (!Stream) - { - buffers = new SoundBuffers(this); - } - else - { - buffers = null; - } - } + public virtual void InitializeAlBuffers() { } - public virtual void FillBuffers() { } + public virtual void FillAlBuffers() { } - public virtual void DeleteALBuffers() + public virtual void DeleteAlBuffers() { Owner.KillChannels(this); buffers?.Dispose(); @@ -165,7 +153,7 @@ namespace Barotrauma.Sounds { if (disposed) { return; } - DeleteALBuffers(); + DeleteAlBuffers(); Owner.RemoveSound(this); disposed = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs index cacd1121a..8282375a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs @@ -7,7 +7,7 @@ using System.Text; namespace Barotrauma.Sounds { - class SoundBuffers : IDisposable + sealed class SoundBuffers : IDisposable { private static readonly HashSet bufferPool = new HashSet(); #if OSX diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 6f5b2efac..b08ed5862 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -312,54 +312,52 @@ namespace Barotrauma.Sounds if (!IsPlaying) { return; } - if (!IsStream) + if (IsStream) { return; } + + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); + Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); + int alError = Al.GetError(); + if (alError != Al.NoError) { - uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); - Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); - int alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } - Al.SourceStop(alSource); + Al.SourceStop(alSource); - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } - if (Sound.Buffers.RequestAlBuffers()) - { - Sound.FillBuffers(); - } - Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer); + Sound.FillAlBuffers(); + if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer); - Al.SourcePlay(alSource); - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } - Al.Sourcei(alSource, Al.SampleOffset, playbackPos); - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + Al.SourcePlay(alSource); + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } + + Al.Sourcei(alSource, Al.SampleOffset, playbackPos); + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -509,10 +507,8 @@ namespace Barotrauma.Sounds throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } - if (Sound.Buffers.RequestAlBuffers()) - { - Sound.FillBuffers(); - } + Sound.FillAlBuffers(); + if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer; Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs index e234f0dc2..ed57ef47b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs @@ -64,14 +64,15 @@ namespace Barotrauma.Sounds /// The b2 value. /// protected double B2; + /// /// The q value. /// - private double _q; + protected readonly double _q; /// /// The gain value in dB. /// - private double _gainDB; + protected readonly double _gainDB; /// /// The z1 value. /// @@ -81,77 +82,15 @@ namespace Barotrauma.Sounds /// protected double Z2; - private double _frequency; - - /// - /// Gets or sets the frequency. - /// - /// value;The samplerate has to be bigger than 2 * frequency. - public double Frequency - { - get { return _frequency; } - set - { - if (SampleRate < value * 2) - { - throw new ArgumentOutOfRangeException("value", "The samplerate has to be bigger than 2 * frequency."); - } - _frequency = value; - CalculateBiQuadCoefficients(); - } - } + protected readonly double _frequency; /// /// Gets the sample rate. /// - public int SampleRate { get; private set; } + protected readonly int _sampleRate; - /// - /// The q value. - /// - public double Q - { - get { return _q; } - set - { - if (value <= 0) - { - throw new ArgumentOutOfRangeException("value"); - } - _q = value; - CalculateBiQuadCoefficients(); - } - } - - /// - /// Gets or sets the gain value in dB. - /// - public double GainDB - { - get { return _gainDB; } - set - { - _gainDB = value; - CalculateBiQuadCoefficients(); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The sample rate. - /// The frequency. - /// - /// sampleRate - /// or - /// frequency - /// or - /// q - /// - protected BiQuad(int sampleRate, double frequency) - : this(sampleRate, frequency, 1.0 / Math.Sqrt(2)) - { - } + protected static readonly double DefaultQ = 1.0 / Math.Sqrt(2); + protected const double DefaultGainDb = 6.0; /// /// Initializes a new instance of the class. @@ -166,18 +105,29 @@ namespace Barotrauma.Sounds /// or /// q /// - protected BiQuad(int sampleRate, double frequency, double q) + protected BiQuad(int sampleRate, double frequency, double q, double gainDb) { if (sampleRate <= 0) + { throw new ArgumentOutOfRangeException("sampleRate"); + } if (frequency <= 0) + { throw new ArgumentOutOfRangeException("frequency"); + } if (q <= 0) + { throw new ArgumentOutOfRangeException("q"); - SampleRate = sampleRate; - Frequency = frequency; - Q = q; - GainDB = 6; + } + if (sampleRate < frequency * 2) + { + throw new ArgumentOutOfRangeException("sampleRate", "The sample rate has to be greater than or equal to 2 * frequency."); + } + _sampleRate = sampleRate; + _frequency = frequency; + _q = q; + _gainDB = gainDb; + CalculateBiQuadCoefficients(); } /// @@ -215,7 +165,7 @@ namespace Barotrauma.Sounds /// /// Used to apply a lowpass-filter to a signal. /// - public class LowpassFilter : BiQuad + public sealed class LowpassFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -223,7 +173,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public LowpassFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -232,20 +182,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - var norm = 1 / (1 + k / Q + k * k); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + var norm = 1 / (1 + k / _q + k * k); A0 = k * k * norm; A1 = 2 * A0; A2 = A0; B1 = 2 * (k * k - 1) * norm; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a highpass-filter to a signal. /// - public class HighpassFilter : BiQuad + public sealed class HighpassFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -253,7 +203,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public HighpassFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -262,20 +212,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - var norm = 1 / (1 + k / Q + k * k); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + var norm = 1 / (1 + k / _q + k * k); A0 = 1 * norm; A1 = -2 * A0; A2 = A0; B1 = 2 * (k * k - 1) * norm; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a bandpass-filter to a signal. /// - public class BandpassFilter : BiQuad + public sealed class BandpassFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -283,7 +233,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public BandpassFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -292,20 +242,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double norm = 1 / (1 + k / Q + k * k); - A0 = k / Q * norm; + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double norm = 1 / (1 + k / _q + k * k); + A0 = k / _q * norm; A1 = 0; A2 = -A0; B1 = 2 * (k * k - 1) * norm; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a notch-filter to a signal. /// - public class NotchFilter : BiQuad + public sealed class NotchFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -313,7 +263,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public NotchFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -322,20 +272,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double norm = 1 / (1 + k / Q + k * k); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double norm = 1 / (1 + k / _q + k * k); A0 = (1 + k * k) * norm; A1 = 2 * (k * k - 1) * norm; A2 = A0; B1 = A1; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a lowshelf-filter to a signal. /// - public class LowShelfFilter : BiQuad + public sealed class LowShelfFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -344,10 +294,8 @@ namespace Barotrauma.Sounds /// The filter's corner frequency. /// Gain value in dB. public LowShelfFilter(int sampleRate, double frequency, double gainDB) - : base(sampleRate, frequency) - { - GainDB = gainDB; - } + : base(sampleRate, frequency, DefaultQ, gainDB) + { } /// /// Calculates all coefficients. @@ -355,10 +303,10 @@ namespace Barotrauma.Sounds protected override void CalculateBiQuadCoefficients() { const double sqrt2 = 1.4142135623730951; - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double v = Math.Pow(10, Math.Abs(GainDB) / 20.0); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double v = Math.Pow(10, Math.Abs(_gainDB) / 20.0); double norm; - if (GainDB >= 0) + if (_gainDB >= 0) { // boost norm = 1 / (1 + sqrt2 * k + k * k); A0 = (1 + Math.Sqrt(2 * v) * k + v * k * k) * norm; @@ -382,7 +330,7 @@ namespace Barotrauma.Sounds /// /// Used to apply a highshelf-filter to a signal. /// - public class HighShelfFilter : BiQuad + public sealed class HighShelfFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -391,10 +339,8 @@ namespace Barotrauma.Sounds /// The filter's corner frequency. /// Gain value in dB. public HighShelfFilter(int sampleRate, double frequency, double gainDB) - : base(sampleRate, frequency) - { - GainDB = gainDB; - } + : base(sampleRate, frequency, DefaultQ, gainDB) + { } /// /// Calculates all coefficients. @@ -402,10 +348,10 @@ namespace Barotrauma.Sounds protected override void CalculateBiQuadCoefficients() { const double sqrt2 = 1.4142135623730951; - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double v = Math.Pow(10, Math.Abs(GainDB) / 20.0); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double v = Math.Pow(10, Math.Abs(_gainDB) / 20.0); double norm; - if (GainDB >= 0) + if (_gainDB >= 0) { // boost norm = 1 / (1 + sqrt2 * k + k * k); A0 = (v + Math.Sqrt(2 * v) * k + k * k) * norm; @@ -429,22 +375,8 @@ namespace Barotrauma.Sounds /// /// Used to apply an peak-filter to a signal. /// - public class PeakFilter : BiQuad + public sealed class PeakFilter : BiQuad { - /// - /// Gets or sets the bandwidth. - /// - public double BandWidth - { - get { return Q; } - set - { - if (value <= 0) - throw new ArgumentOutOfRangeException("value"); - Q = value; - } - } - /// /// Initializes a new instance of the class. /// @@ -453,10 +385,8 @@ namespace Barotrauma.Sounds /// The bandWidth. /// The gain value in dB. public PeakFilter(int sampleRate, double frequency, double bandWidth, double peakGainDB) - : base(sampleRate, frequency, bandWidth) - { - GainDB = peakGainDB; - } + : base(sampleRate, frequency, bandWidth, peakGainDB) + { } /// /// Calculates all coefficients. @@ -464,11 +394,11 @@ namespace Barotrauma.Sounds protected override void CalculateBiQuadCoefficients() { double norm; - double v = Math.Pow(10, Math.Abs(GainDB) / 20.0); - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double q = Q; + double v = Math.Pow(10, Math.Abs(_gainDB) / 20.0); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double q = _q; - if (GainDB >= 0) //boost + if (_gainDB >= 0) //boost { norm = 1 / (1 + 1 / q * k + k * k); A0 = (1 + v / q * k + k * k) * norm; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 575e822eb..e6cefeb2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -812,7 +812,7 @@ namespace Barotrauma.Sounds { for (int i = loadedSounds.Count - 1; i >= 0; i--) { - loadedSounds[i].InitializeALBuffers(); + loadedSounds[i].InitializeAlBuffers(); } } @@ -834,7 +834,7 @@ namespace Barotrauma.Sounds { if (keepSounds) { - loadedSounds[i].DeleteALBuffers(); + loadedSounds[i].DeleteAlBuffers(); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index f53f49be0..e04b5a60f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -156,6 +156,12 @@ namespace Barotrauma insideSubFactor = 1.0f; } + if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead) + { + //make the sound lerp to the "outside" sound when under pressure + insideSubFactor -= Character.Controlled.PressureTimer / 100.0f; + } + movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); if (!MathUtils.IsValid(movementSoundVolume)) { @@ -183,7 +189,7 @@ namespace Barotrauma if (chn is null || !chn.IsPlaying) { if (volume < 0.01f) { return; } - if (!(chn is null)) { waterAmbienceChannels.Remove(chn); } + if (chn is not null) { waterAmbienceChannels.Remove(chn); } chn = sound.Play(volume, "waterambience"); chn.Looping = true; waterAmbienceChannels.Add(chn); @@ -195,6 +201,15 @@ namespace Barotrauma { chn.FadeOutAndDispose(); } + if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead) + { + //make the sound decrease in pitch when under pressure + chn.FrequencyMultiplier = MathHelper.Clamp(Character.Controlled.PressureTimer / 200.0f, 0.75f, 1.0f); + } + else + { + chn.FrequencyMultiplier = Math.Min(chn.frequencyMultiplier + deltaTime, 1.0f); + } } } @@ -652,6 +667,7 @@ namespace Barotrauma } } + LogCurrentMusic(); updateMusicTimer = UpdateMusicInterval; } @@ -715,6 +731,26 @@ namespace Barotrauma } } + private static double lastMusicLogTime; + const double MusicLogInterval = 60.0; + private static void LogCurrentMusic() + { + if (Screen.Selected != GameMain.GameScreen) { return; } + if (Timing.TotalTime < lastMusicLogTime + MusicLogInterval) { return; } + for (int i = 0; i < musicChannel.Length; i++) + { + if (musicChannel[i] != null && + musicChannel[i].IsPlaying && + musicChannel[i].Sound?.Filename != null) + { + GameAnalyticsManager.AddDesignEvent( + "BackgroundMusic:" + + Path.GetFileNameWithoutExtension(musicChannel[i].Sound.Filename.Replace(":", string.Empty).Replace(" ", string.Empty))); + } + } + lastMusicLogTime = Timing.TotalTime; + } + private static void DisposeMusicChannel(int index) { var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 82c294c6e..e599e9c41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -10,6 +10,45 @@ namespace Barotrauma { public partial class Sprite { + public Identifier Identifier { get; private set; } + public static IEnumerable LoadedSprites + { + get + { + List retVal = null; + lock (list) + { + retVal = list.Select(wRef => + { + if (wRef.TryGetTarget(out Sprite spr)) + { + return spr; + } + return null; + }).Where(s => s != null).ToList(); + } + return retVal; + } + } + + private static readonly List> list = new List>(); + + static partial void AddToList(Sprite elem) + { + lock (list) + { + list.Add(new WeakReference(elem)); + } + } + + static partial void RemoveFromList(Sprite sprite) + { + lock (list) + { + list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == sprite); + } + } + private class TextureRefCounter { public Texture2D Texture; @@ -130,6 +169,11 @@ namespace Barotrauma public void ReloadTexture() { var oldTexture = texture; + if (texture == null) + { + DebugConsole.ThrowError("Sprite: Failed to reload the texture, texture is null."); + return; + } texture.Dispose(); texture = TextureLoader.FromFile(FilePath.Value, Compress); Identifier pathKey = FullPath.ToIdentifier(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 831abe5b3..2748b691c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -114,7 +115,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); - currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); + currentLobby?.SetData("traitors", serverSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture)); currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 918437918..44506e13a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -69,7 +69,7 @@ namespace Barotrauma.Steam //Steamworks is completely insane so the following needs comments: //This callback seems to take place when the item in question has not been downloaded recently - Steamworks.SteamUGC.GlobalOnItemInstalled = id => Workshop.OnItemDownloadComplete(id); + Steamworks.SteamUGC.OnItemInstalled += (appId, itemId) => Workshop.OnItemDownloadComplete(itemId); //This callback seems to take place when the item has been downloaded recently and an update //or a redownload has taken place diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index a04a3e1bf..b7a66a8b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -379,7 +379,7 @@ namespace Barotrauma.Steam private IEnumerable MessageBoxCoroutine(Func> subcoroutine) { - var messageBox = new GUIMessageBox("", "...", buttons: new [] { TextManager.Get("Cancel") }); + var messageBox = new GUIMessageBox("", TextManager.Get("ellipsis"), buttons: new [] { TextManager.Get("Cancel") }); messageBox.Buttons[0].OnClicked = (button, o) => { messageBox.Close(); @@ -528,10 +528,18 @@ namespace Barotrauma.Steam yield return new WaitForSeconds(0.5f); } - if (!resultItem.IsInstalled) + //there seems to sometimes be a brief delay between the download task and the item being installed, wait a bit before deeming the install as failed + DateTime waitInstallUntil = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + while (!resultItem.IsInstalled || resultItem.IsDownloading) { - throw new Exception($"Failed to install item: download task ended with status {downloadTask.Status}, " + - $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); + if (DateTime.Now > waitInstallUntil) + { + throw new Exception($"Failed to install item: download task ended with status {downloadTask.Status}," + + $" item installed: {resultItem.IsInstalled}, " + + $" item downloading: {resultItem.IsDownloading}, " + + $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); + } + yield return new WaitForSeconds(0.5f); } ContentPackage? pkgToNuke diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index fac6cd8cf..b7890da55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Steam var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "", createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, textColor: Color.DarkGray * 0.6f, - text: TextManager.Get("Search") + "...", + text: TextManager.Get("Search") + TextManager.Get("ellipsis"), textAlignment: Alignment.CenterLeft) { CanBeFocused = false diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs new file mode 100644 index 000000000..3bad00872 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Barotrauma.Networking; + +namespace Barotrauma +{ + sealed partial class TraitorManager + { + public static void ClientRead(IReadMessage msg) + { + //unused, but could be worth keeping in the messages regardless in case a mod wants to use these for something + TraitorEvent.State state = (TraitorEvent.State)msg.ReadByte(); + Identifier eventIdentifier = msg.ReadIdentifier(); + if (GameMain.Client?.Character == null) + { + DebugConsole.ThrowError("Received a traitor update when not controlling a character."); + return; + } + GameMain.Client.Character.IsTraitor = true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs deleted file mode 100644 index c427adf4b..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml.Linq; - -namespace Barotrauma -{ - class TraitorMissionPrefab : Prefab - { - public static readonly PrefabCollection Prefabs = new PrefabCollection(); - - public readonly Sprite Icon; - public readonly Color IconColor; - - public TraitorMissionPrefab(ContentXElement element, TraitorMissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) - { - foreach (var subElement in element.Elements()) - { - if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) - { - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); - } - } - } - - public override void Dispose() - { - Icon?.Remove(); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs deleted file mode 100644 index eaea08977..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Barotrauma.Networking; -using System; - -namespace Barotrauma -{ - partial class TraitorMissionResult - { - public TraitorMissionResult(IReadMessage inc) - { - MissionIdentifier = inc.ReadIdentifier(); - EndMessage = inc.ReadString(); - Success = inc.ReadBoolean(); - byte characterCount = inc.ReadByte(); - for (int i = 0; i < characterCount; i++) - { - UInt16 characterID = inc.ReadUInt16(); - var character = Entity.FindEntityByID(characterID) as Character; - if (character != null) { Characters.Add(character); } - } - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 67bc81568..63473abd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -94,8 +94,7 @@ namespace Barotrauma if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) { StatusEffect statusEffect = StatusEffect.Load(subElement, debugIdentifier); - if (statusEffect == null || !statusEffect.HasTag("medical")) { continue; } - + if (statusEffect == null || !statusEffect.HasTag(Tags.MedicalItem)) { continue; } statusEffects.Add(statusEffect); } else if (IsRequiredSkill(subElement, out Skill? skill) && skill != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 0ba972b3f..1fcbab5fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -1,8 +1,7 @@ -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using Barotrauma.IO; using System.Threading.Tasks; +using Barotrauma.IO; using Lidgren.Network; using Color = Microsoft.Xna.Framework.Color; @@ -45,155 +44,117 @@ namespace Barotrauma private static byte[] CompressDxt5(byte[] data, int width, int height) { - using (System.IO.MemoryStream mstream = new System.IO.MemoryStream()) - { - for (int y = 0; y < height; y += 4) + var output = new byte[width * height]; + Parallel.For( + fromInclusive: 0, + toExclusive: width * height / 16, + i => { - for (int x = 0; x < width; x += 4) - { - int offset = x * 4 + y * 4 * width; - CompressDxt5Block(data, offset, width, mstream); - } - } - return mstream.ToArray(); - } + int i4 = i * 4; + int inputOffset = (i4 % width + (i4 / width) * 4 * width) * 4; + int outputOffset = i * 16; + CompressDxt5Block(data, inputOffset, width, output, outputOffset); + }); + return output; } - private static void CompressDxt5Block(byte[] data, int offset, int width, System.IO.Stream output) + private static void CompressDxt5Block(byte[] data, int inputOffset, int width, byte[] output, int outputOffset) { int r1 = 255, g1 = 255, b1 = 255, a1 = 255; int r2 = 0, g2 = 0, b2 = 0, a2 = 0; - //determine the two colors to interpolate between: - //color 1 represents lowest luma, color 2 represents highest luma + // Determine the two colors to interpolate between: + // color 1 represents lowest luma, color 2 represents highest luma. + // Luma is also used to determine which color on the palette + // most closely resembles each pixel to compress, so we + // cache our calculations here. + int y1 = 255000; + int y2 = 0; for (int i = 0; i < 16; i++) { - int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); - int r, g, b, a; - r = data[pixelOffset + 0]; - g = data[pixelOffset + 1]; - b = data[pixelOffset + 2]; - a = data[pixelOffset + 3]; - if (r * 299 + g * 587 + b * 114 < r1 * 299 + g1 * 587 + b1 * 114) + int pixelOffset = inputOffset + (4 * ((i % 4) + (width * (i >> 2)))); + int r = data[pixelOffset + 0]; + int g = data[pixelOffset + 1]; + int b = data[pixelOffset + 2]; + int a = data[pixelOffset + 3]; + int y = r * 299 + g * 587 + b * 114; + if (y < y1) { - r1 = r; g1 = g; b1 = b; + r1 = r; g1 = g; b1 = b; y1 = y; } - if (r * 299 + g * 587 + b * 114 > r2 * 299 + g2 * 587 + b2 * 114) + if (y > y2) { - r2 = r; g2 = g; b2 = b; + r2 = r; g2 = g; b2 = b; y2 = y; } if (a < a1) { a1 = a; } if (a > a2) { a2 = a; } } //convert the colors to rgb565 (16-bit rgb) - int r1_565 = (r1 * 0x1f) / 0xff; if (r1_565 > 0x1f) { r1_565 = 0x1f; } - int g1_565 = (g1 * 0x3f) / 0xff; if (g1_565 > 0x3f) { g1_565 = 0x3f; } - int b1_565 = (b1 * 0x1f) / 0xff; if (b1_565 > 0x1f) { b1_565 = 0x1f; } + int r1_565 = r1 >> (8 - 5); + int g1_565 = g1 >> (8 - 6); + int b1_565 = b1 >> (8 - 5); - int r2_565 = (r2 * 0x1f) / 0xff; if (r2_565 > 0x1f) { r2_565 = 0x1f; } - int g2_565 = (g2 * 0x3f) / 0xff; if (g2_565 > 0x3f) { g2_565 = 0x3f; } - int b2_565 = (b2 * 0x1f) / 0xff; if (b2_565 > 0x1f) { b2_565 = 0x1f; } + int r2_565 = r2 >> (8 - 5); + int g2_565 = g2 >> (8 - 6); + int b2_565 = b2 >> (8 - 5); - //luma is also used to determine which color on the palette - //most closely resembles each pixel to compress, so we - //calculate this here - int y1 = r1 * 299 + g1 * 587 + b1 * 114; - int y2 = r2 * 299 + g2 * 587 + b2 * 114; - - byte[] newData = new byte[16]; - for (int i = 0; i < 16; i++) + int y2y1Diff = y2 - y1; + if (y2y1Diff > 0 || a1 < a2) { - int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); - int r, g, b, a; - r = data[pixelOffset + 0]; - g = data[pixelOffset + 1]; - b = data[pixelOffset + 2]; - a = data[pixelOffset + 3]; - - if (a1 < a2) + for (int i = 0; i < 16; i++) { - a -= a1; - a = (a * 0x7) / (a2 - a1); - if (a > 0x7) { a = 0x7; } + int pixelOffset = inputOffset + (4 * ((i % 4) + (width * (i >> 2)))); + int r = data[pixelOffset + 0]; + int g = data[pixelOffset + 1]; + int b = data[pixelOffset + 2]; - switch (a) + if (a1 < a2) { - case 0: - a = 1; - break; - case 1: - a = 7; - break; - case 2: - a = 6; - break; - case 3: - a = 5; - break; - case 4: - a = 4; - break; - case 5: - a = 3; - break; - case 6: - a = 2; - break; - case 7: - a = 0; - break; + int a = data[pixelOffset + 3]; + a -= a1; + a = (a * 0x7) / (a2 - a1); + if (a < 0x7) + { + a = a switch + { + 0 => 1, + 1 => 7, + _ => 8 - a + }; + NetBitWriter.WriteByte((byte)a, 3, output, (outputOffset * 8) + 16 + (i * 3)); + } } - } - else - { - a = 0; - } - NetBitWriter.WriteUInt32((uint)a, 3, newData, 16 + (i * 3)); + if (y2y1Diff <= 0) { continue; } - int y = r * 299 + g * 587 + b * 114; - - int max = y2 - y1; - int diffY = y - y1; - - int paletteIndex; - if (diffY < max / 4) - { - paletteIndex = 0; + int y = r * 299 + g * 587 + b * 114; + int diffY = y - y1; + int paletteIndex = (diffY * 4) / y2y1Diff; + paletteIndex = paletteIndex switch + { + 0 => 0, + 1 => 2, + 2 => 3, + _ => 1 + }; + output[outputOffset + 12 + (i / 4)] |= (byte)(paletteIndex << (2 * (i % 4))); } - else if (diffY < max / 2) - { - paletteIndex = 2; - } - else if (diffY < max * 3 / 4) - { - paletteIndex = 3; - } - else - { - paletteIndex = 1; - } - newData[12 + (i / 4)] |= (byte)(paletteIndex << (2 * (i % 4))); } - newData[0] = (byte)a2; - newData[1] = (byte)a1; + output[outputOffset + 0] = (byte)a2; + output[outputOffset + 1] = (byte)a1; - newData[9] = (byte)((r1_565 << 3) | (g1_565 >> 3)); - newData[8] = (byte)((g1_565 << 5) | b1_565); - newData[11] = (byte)((r2_565 << 3) | (g2_565 >> 3)); - newData[10] = (byte)((g2_565 << 5) | b2_565); - - output.Write(newData, 0, 16); + output[outputOffset + 9] = (byte)((r1_565 << 3) | (g1_565 >> 3)); + output[outputOffset + 8] = (byte)((g1_565 << 5) | b1_565); + output[outputOffset + 11] = (byte)((r2_565 << 3) | (g2_565 >> 3)); + output[outputOffset + 10] = (byte)((g2_565 << 5) | b2_565); } public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false) { - using (FileStream fileStream = File.OpenRead(path)) - { - return FromStream(fileStream, path, compress, mipmap); - } + using FileStream fileStream = File.OpenRead(path); + return FromStream(fileStream, path, compress, mipmap); } public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index c5342a37c..c89d0cbfb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -142,10 +142,10 @@ namespace Barotrauma GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); - DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); - DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f))); - DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true)); - DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true)); + DrawBatch(() => Submarine.DrawBack(spriteBatch, editing: false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); + DrawBatch(() => Submarine.DrawBack(spriteBatch, editing: false, e => (e is not Structure || e.SpriteDepth < 0.9f))); + DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: false)); + DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: false)); void DrawBatch(Action drawAction) { diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb index 9951e2186b56992ef5c477c02a0f176eb5820b01..47c98fc61d39a2668da9eb84be21a61e5c5ea698 100644 GIT binary patch delta 50 zcmV-20L}mK67Uj`l?12k9y*bk9uPH4c6n+a delta 50 zcmV-20L}mK67Uj`l>|ld-inc#9uT+V8AE4HiQW-1X!nPaXFjpVY5@UNvoZn&0|His IvpEGf1VYRc#sB~S diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb index 0426d9e092d61a1cf5eac89bb2cbcd1ffc89a00b..e09186ade6f28719606114b3a375b33101e4d6d7 100644 GIT binary patch delta 25 fcmX>jbVg{xRF?3$PgOR~T*b^%0R)@fSy)*Cj>ZYb delta 25 fcmX>jbVg{xRF)ZwDp@zqT*b^%2?U$nSy)*Ci#iE~ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 4b5dccfa1..aa20c1261 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -62,6 +62,7 @@ + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6d8354723..75a4a7c97 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -64,6 +64,7 @@ + @@ -134,18 +135,13 @@ - - - - libsteam_api64.dylib - PreserveNewest - - PreserveNewest + + diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx index 6d08d4460..d6a3e31ff 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx @@ -1,50 +1,50 @@ - -Texture2D xTexture; -sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; - -Texture2D xStencil; -sampler StencilSampler = sampler_state { Texture = ; }; - -float aCutoff; -float4x4 wearableUvToClipperUv; -float clipperTexelSize; - -float2 stencilUVmin, stencilUVmax; - -float stencilSample(float2 texCoord, float2 offset) -{ - return xStencil.Sample( - StencilSampler, - mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; -} - -float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 -{ - float4 c = xTexture.Sample(TextureSampler, texCoord) * color; - - float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; - clip(stencilUV.x - stencilUVmin.x); - clip(stencilUV.y - stencilUVmin.y); - clip(stencilUVmax.y - stencilUV.x); - clip(stencilUVmax.y - stencilUV.y); - - float minStencil = stencilSample(texCoord, float2(0,0)); - minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); - - float aDiff = minStencil - aCutoff; - - clip(aDiff); - - return c; -} - -technique StencilShader -{ - pass Pass1 - { - PixelShader = compile ps_4_0_level_9_1 main(); - } -} + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float2 stencilUVmin, stencilUVmax; + +float stencilSample(float2 texCoord, float2 offset) +{ + return xStencil.Sample( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * color; + + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.x - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx index d845e79d3..72b633bee 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx @@ -1,50 +1,50 @@ - -Texture2D xTexture; -sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; - -Texture2D xStencil; -sampler StencilSampler = sampler_state { Texture = ; }; - -float aCutoff; -float4x4 wearableUvToClipperUv; -float clipperTexelSize; - -float2 stencilUVmin, stencilUVmax; - -float stencilSample(float2 texCoord, float2 offset) -{ - return tex2D( - StencilSampler, - mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; -} - -float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 -{ - float4 c = tex2D(TextureSampler, texCoord) * color; - - float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; - clip(stencilUV.x - stencilUVmin.x); - clip(stencilUV.y - stencilUVmin.y); - clip(stencilUVmax.y - stencilUV.x); - clip(stencilUVmax.y - stencilUV.y); - - float minStencil = stencilSample(texCoord, float2(0,0)); - minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); - - float aDiff = minStencil - aCutoff; - - clip(aDiff); - - return c; -} - -technique StencilShader -{ - pass Pass1 - { - PixelShader = compile ps_2_0 main(); - } -} + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float2 stencilUVmin, stencilUVmax; + +float stencilSample(float2 texCoord, float2 offset) +{ + return tex2D( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * color; + + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.x - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_2_0 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index cec27c949..77b4d8ebb 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -70,6 +70,7 @@ + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f1b4c157c..5246e818e 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -56,6 +56,7 @@ + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 592c084d0..1959e3b21 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -60,6 +60,7 @@ + @@ -87,12 +88,6 @@ - - - libsteam_api64.dylib - PreserveNewest - - diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index c597f599e..d61c718ca 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -376,10 +376,9 @@ namespace Barotrauma tempBuffer.WriteBoolean(aiming); tempBuffer.WriteBoolean(shoot); tempBuffer.WriteBoolean(use); - if (AnimController is HumanoidAnimController) - { - tempBuffer.WriteBoolean(((HumanoidAnimController)AnimController).Crouching); - } + + tempBuffer.WriteBoolean(AnimController is HumanoidAnimController { Crouching: true }); + tempBuffer.WriteBoolean(attack); Vector2 relativeCursorPos = cursorPosition - AimRefPosition; @@ -430,21 +429,31 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); - AIController?.ServerWrite(tempBuffer); + tempBuffer.WriteBoolean(AIController is EnemyAIController); + if (AIController is EnemyAIController enemyAi) + { + tempBuffer.WriteByte((byte)enemyAi.State); + tempBuffer.WriteBoolean(enemyAi.PetBehavior is PetBehavior); + if (enemyAi.PetBehavior is PetBehavior petBehavior) + { + tempBuffer.WriteByte((byte)((petBehavior.Happiness / petBehavior.MaxHappiness) * byte.MaxValue)); + tempBuffer.WriteByte((byte)((petBehavior.Hunger / petBehavior.MaxHunger) * byte.MaxValue)); + } + } HealthUpdatePending = false; } } public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); } + if (extraData is not IEventData eventData) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); } msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); switch (eventData) { - case InventoryStateEventData _: + case InventoryStateEventData inventoryData: msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - Inventory.ServerEventWrite(msg, c); + Inventory.ServerEventWrite(msg, c, inventoryData); break; case ControlEventData controlEventData: Client owner = controlEventData.Owner; @@ -473,12 +482,12 @@ namespace Barotrauma case IAttackEventData attackEventData: { int attackLimbIndex = Removed ? -1 : Array.IndexOf(AnimController.Limbs, attackEventData.AttackLimb); - ushort targetEntityId = 0; + ushort targetEntityId = NullEntityID; int targetLimbIndex = -1; if (attackEventData.TargetEntity is Entity { Removed: false } targetEntity) { targetEntityId = targetEntity.ID; - if (targetEntity is Character { AnimController: { Limbs: var targetLimbsArray } }) + if (targetEntity is Character { AnimController.Limbs: var targetLimbsArray }) { targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs new file mode 100644 index 000000000..8232dd5e9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs @@ -0,0 +1,16 @@ +#nullable enable + +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + internal abstract partial class CircuitBoxConnection + { + public string Name => Connection.Name; + + private partial void InitProjSpecific(CircuitBox circuitBox) + { + Length = 100f; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 0411715e8..fd59111ec 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Text; using Barotrauma.Steam; +using Barotrauma.Extensions; namespace Barotrauma { @@ -1020,11 +1021,6 @@ namespace Barotrauma client.SpectateOnly = false; }); - AssignOnExecute("starttraitormissionimmediately", (string[] args) => - { - GameMain.Server?.TraitorManager?.SkipStartDelay(); - }); - AssignOnExecute("difficulty|leveldifficulty", (string[] args) => { if (GameMain.Server == null || args.Length < 1) return; @@ -1082,56 +1078,83 @@ namespace Barotrauma GameMain.Server?.UpdateCheatsEnabled(); }); - commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their targets.", (string[] args) => + + AssignOnExecute("triggertraitorevent", (string[] args) => { - if (GameMain.Server == null) return; - TraitorManager traitorManager = GameMain.Server.TraitorManager; - if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) + if (GameMain.Server?.TraitorManager == null) { - NewMessage("There are no traitors at the moment.", Color.Cyan); + ThrowError($"Could not start a traitor event. {nameof(TraitorManager)} hasn't been created."); return; } - foreach (Traitor t in traitorManager.Traitors) + if (args.Length > 0) { - if (t.CurrentObjective != null) + Identifier traitorEventId = args[0].ToIdentifier(); + if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab prefab) && prefab is TraitorEventPrefab traitorEventPrefab) { - NewMessage(string.Format("- Traitor {0}'s current goals are:\n{1}", t.Character.Name, t.CurrentObjective.GoalInfos), Color.Cyan); + GameMain.Server?.TraitorManager?.ForceTraitorEvent(traitorEventPrefab); } else { - NewMessage(string.Format("- Traitor {0} has no current objective.", t.Character.Name), Color.Cyan); + ThrowError($"Could not find a traitor event prefab with the identifier \"{traitorEventId}\"."); + return; } } - //NewMessage("The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", Color.Cyan); + if (GameMain.Server?.TraitorManager is { } traitorManager) + { + traitorManager.Enabled = true; + traitorManager.SkipStartDelay(); + } + }); + + commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their current objectives.", (string[] args) => + { + if (GameMain.Server == null) { return; } + CreateTraitorList((string msg) => NewMessage(msg, Color.Cyan)); })); AssignOnClientRequestExecute("traitorlist", (Client client, Vector2 cursorPos, string[] args) => { - TraitorManager traitorManager = GameMain.Server.TraitorManager; - if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) + if (GameMain.Server == null) { return; } + CreateTraitorList((string msg) => { - GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", Identifier.Empty, TraitorMessageType.Console); + GameMain.Server.SendDirectChatMessage(msg, client); + }); + }); + + void CreateTraitorList(Action createMessage) + { + if (GameMain.Server == null) return; + TraitorManager traitorManager = GameMain.Server.TraitorManager; + if (traitorManager == null || traitorManager.ActiveEvents.None()) + { + createMessage("There are no traitors at the moment."); return; } - foreach (Traitor t in traitorManager.Traitors) + createMessage("Traitors:"); + foreach (var ev in traitorManager.ActiveEvents) { - if (t.CurrentObjective != null) + createMessage($" - {ev.Traitor.Name}: {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState})"); + } + } + + AssignOnClientRequestExecute( + "debugevent", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (GameMain.Server == null) { return; } + if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0) { - var traitorGoals = TextManager.FormatServerMessage(t.CurrentObjective.GoalInfos); - var traitorGoalsStart = traitorGoals.LastIndexOf('/') + 1; - GameMain.Server.SendTraitorMessage(client, string.Join("/", new[] { - traitorGoals.Substring(0, traitorGoalsStart), - $"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}", - $"[traitorname]={t.Character.Name}", - "Traitor [traitorname]'s current goals are:\n[traitorgoals]" - }.Where(s => !string.IsNullOrEmpty(s))), t.Mission.Identifier, TraitorMessageType.Console); - } - else - { - GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", t.Character.Name), Identifier.Empty, TraitorMessageType.Console); + var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]); + if (ev == null) + { + GameMain.Server.SendConsoleMessage($"Event \"{args[0]}\" not found.", client); + } + else + { + GameMain.Server.SendConsoleMessage(ev.GetDebugInfo(), client); + } } } - //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); - }); + ); commands.Add(new Command("setpassword|setserverpassword|password", "setpassword [password]: Changes the password of the server that's being hosted.", (string[] args) => { @@ -2401,7 +2424,7 @@ namespace Barotrauma } })); - commands.Add(new Command("sendchatmessage", "Sends a chat message with specified type and color.", (string[] args) => + commands.Add(new Command("sendchatmessage", "sendchatmessage [sendername] [message] [type] [r] [g] [b] [a]: Sends a chat message with specified type and color.", (string[] args) => { if (args.Length < 2) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 353ef08d8..39fa32cab 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -132,7 +132,7 @@ namespace Barotrauma else { outmsg.WriteUInt16(speaker?.ID ?? Entity.NullEntityID); - outmsg.WriteString(Text ?? string.Empty); + outmsg.WriteString(GetDisplayText()?.Value ?? string.Empty); outmsg.WriteBoolean(FadeToBlack); outmsg.WriteByte((byte)Options.Count); for (int i = 0; i < Options.Count; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs new file mode 100644 index 000000000..976984e76 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -0,0 +1,55 @@ +#nullable enable + +using Barotrauma.Extensions; +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class EventLogAction : EventAction +{ + partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) + { + if (eventLog == null) { return; } + if (!TargetTag.IsEmpty) + { + List targetClients = new List(); + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is Character character) + { + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + if (ownerClient != null && eventLog != null) + { + targetClients.Add(ownerClient); + } + } + else + { + DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character."); + } + } + if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) + { + Log(targetClients); + } + } + else + { + if (eventLog != null && eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog) + { + Log(targetClients: null); + } + } + + void Log(List? targetClients) + { + string clientStr = targetClients == null || targetClients.None() ? + string.Empty : + $" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})"; + GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText, + ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs new file mode 100644 index 000000000..9b84d31dd --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs @@ -0,0 +1,36 @@ +namespace Barotrauma +{ + partial class EventObjectiveAction : EventAction + { + partial void UpdateProjSpecific() + { + if (GameMain.Server == null) { return; } + EventManager.NetEventObjective objective = new EventManager.NetEventObjective( + Type, + Identifier, + ObjectiveTag, + TextTag, + ParentObjectiveId, + CanBeCompleted); + + if (TargetTag.IsEmpty) + { + foreach (var client in GameMain.Server.ConnectedClients) + { + if (client.Character == null) { continue; } + EventManager.ServerWriteObjective(client, objective); + } + } + else + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is not Character character) { continue; } + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + if (ownerClient == null) { continue; } + EventManager.ServerWriteObjective(ownerClient, objective); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs new file mode 100644 index 000000000..6e8d0a8a1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma; + +partial class EventLog +{ + public bool TryAddEntry(Identifier eventPrefabId, Identifier entryId, string text, IEnumerable targetClients) + { + if (TryAddEntryInternal(eventPrefabId, entryId, text)) + { + foreach (var targetClient in targetClients) + { + EventManager.ServerWriteEventLog(targetClient, new EventManager.NetEventLogEntry(eventPrefabId, entryId, text)); + } + return true; + } + return false; + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 62b7f9882..74549b187 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -1,12 +1,29 @@ using Barotrauma.Networking; using System; -using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class EventManager { + public static void ServerWriteEventLog(Client client, NetEventLogEntry entry) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)NetworkEventType.EVENTLOG); + outmsg.WriteNetSerializableStruct(entry); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + + public static void ServerWriteObjective(Client client, NetEventObjective entry) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)NetworkEventType.EVENTOBJECTIVE); + outmsg.WriteNetSerializableStruct(entry); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + public void ServerRead(IReadMessage inc, Client sender) { UInt16 actionId = inc.ReadUInt16(); @@ -16,14 +33,14 @@ namespace Barotrauma { if (ev is not ScriptedEvent scriptedEvent) { continue; } - var actions = FindActions(scriptedEvent); - foreach (EventAction action in actions.Select(a => a.Item2)) + var actions = scriptedEvent.GetAllActions(); + foreach (EventAction action in actions.Select(a => a.action)) { if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; } if (!convAction.TargetClients.Contains(sender)) { #if DEBUG || UNSTABLE - DebugConsole.ThrowError($"Client \"{sender.Name}\" tried to respond to a ConversationAction that was not targeted to them."); + DebugConsole.ThrowError($"Client \"{sender.Name}\" tried to respond to a ConversationAction that was not targeted to them ({convAction.Text})."); #endif continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index 433b33075..64f8f99b6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -1,4 +1,7 @@ using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -20,6 +23,99 @@ namespace Barotrauma GameServer.Log($"{TextManager.Get("MissionInfo")}: {header} - {message}", ServerLog.MessageType.ServerMessage); } + public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) + { + int remainingRewards = totalReward; + float sum = GetRewardDistibutionSum(crew); + if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } + foreach (Character character in crew) + { + int rewardDistribution = character.Wallet.RewardDistribution; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; + int reward = Math.Min(remainingRewards, (int)(totalReward * rewardWeight)); + character.Wallet.Give(reward); + remainingRewards -= reward; + if (remainingRewards <= 0) { break; } + } + + return remainingRewards; + } + + partial void DistributeExperienceToCrew(IEnumerable crew, int experienceGain) + { + Dictionary traitorExpSteal = new Dictionary(); + float totalExpSteal = 0.0f; + foreach (var traitorEvent in GameMain.Server.TraitorManager.ActiveEvents) + { + if (traitorEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed) { continue; } + if (traitorEvent.Traitor?.Character == null || !GameMain.Server.ConnectedClients.Contains(traitorEvent.Traitor)) { continue; } + + float expSteal = Math.Max(traitorEvent.TraitorEvent.Prefab.StealPercentageOfExperience, 0.0f); + AddTraitorExpSteal(traitorEvent.Traitor.Character, expSteal); + foreach (var secondaryTraitor in traitorEvent.TraitorEvent.SecondaryTraitors) + { + AddTraitorExpSteal(secondaryTraitor.Character, expSteal); + } + + void AddTraitorExpSteal(Character traitorCharacter, float expSteal) + { + if (traitorCharacter == null) { return; } + if (!traitorExpSteal.ContainsKey(traitorCharacter)) + { + traitorExpSteal.Add(traitorCharacter, 0.0f); + } + traitorExpSteal[traitorCharacter] += expSteal; + } + } + totalExpSteal = traitorExpSteal.Values.Sum(); + //if exp to steal exceeds 100%, normalize to get it back to 100% + //(e.g. two traitors who both steal 75%, they'll share 50% of all the exp gains) + if (totalExpSteal > 100.0f) + { + foreach (Character traitor in traitorExpSteal.Keys) + { + traitorExpSteal[traitor] /= totalExpSteal; + } + totalExpSteal = 100.0f; + } + if (totalExpSteal > 0) + { + GameServer.Log($"Traitors stole {(int)totalExpSteal}% of the total experience.", ServerLog.MessageType.Traitors); + } + + int nonTraitorCount = GameSession.GetSessionCrewCharacters(CharacterType.Both).Count(c => !traitorExpSteal.ContainsKey(c)); + foreach (Networking.Client c in GameMain.Server.ConnectedClients) + { + //give the experience to the stored characterinfo if the client isn't currently controlling a character + GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo); + } + foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + { + GiveMissionExperience(bot.Info); + } + + void GiveMissionExperience(CharacterInfo info) + { + if (info == null) { return; } + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + + int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value); + if (info.Character != null && traitorExpSteal.TryGetValue(info.Character, out float expToSteal)) + { + int stealAmount = (int)(experienceGain * nonTraitorCount * expToSteal / 100.0f); + GameServer.Log($"Traitor {info.Character} stole {stealAmount} ({(int)expToSteal}%) of the total experience.", ServerLog.MessageType.Traitors); + finalExperienceGain += stealAmount; + } + else + { + GameServer.Log($"{(int)(finalExperienceGain * totalExpSteal / 100.0f)} ({(int)totalExpSteal}%) was stolen from {info.Name}.", ServerLog.MessageType.Traitors); + finalExperienceGain -= (int)(finalExperienceGain * totalExpSteal / 100.0f); + } + info.GiveExperience(finalExperienceGain); + } + } + public virtual void ServerWriteInitial(IWriteMessage msg, Client c) { msg.WriteUInt16((ushort)State); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index 35649469e..32a7eef1c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -4,8 +4,6 @@ namespace Barotrauma { partial class NestMission : Mission { - private Level.Cave selectedCave; - public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index f41f62988..d21e80fbd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -327,6 +327,7 @@ namespace Barotrauma while (Timing.Accumulator >= Timing.Step) { Timing.TotalTime += Timing.Step; + Timing.TotalTimeUnpaused += Timing.Step; DebugConsole.Update(); if (GameSession?.GameMode == null || !GameSession.GameMode.Paused) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index d99340d6b..ddc0ae1d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -42,10 +42,13 @@ namespace Barotrauma } } - public void Refresh(Character character) + public void Refresh(Character character, bool refreshHealthData) { - healthData = new XElement("health"); - character.CharacterHealth.Save(healthData); + if (refreshHealthData) + { + healthData = new XElement("health"); + character.CharacterHealth.Save(healthData); + } if (character.Inventory != null) { itemData = new XElement("inventory"); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 7629dd08f..e1b3de615 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -260,7 +260,7 @@ namespace Barotrauma { //character still alive (or killed by Disconnect) -> save it as-is characterData.RemoveAll(cd => cd.IsDuplicate(data)); - data.Refresh(character); + data.Refresh(character, refreshHealthData: character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected); characterData.Add(data); } else @@ -318,7 +318,7 @@ namespace Barotrauma discardedCharacters.Clear(); } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { IncrementAllLastUpdateIds(); @@ -360,7 +360,7 @@ namespace Barotrauma GameMain.GameSession.EventManager.RegisterEventHistory(); } - GameMain.GameSession.EndRound("", traitorResults, transitionType); + GameMain.GameSession.EndRound("", transitionType); //-------------------------------------- @@ -1361,6 +1361,10 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + if (GameMain.Server?.TraitorManager is TraitorManager traitorManager) + { + modeElement.Add(traitorManager.Save()); + } modeElement.Add(Bank.Save()); if (GameMain.GameSession?.EventManager != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs new file mode 100644 index 000000000..a00978616 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class CharacterInventory : Inventory + { + public void ServerEventWrite(IWriteMessage msg, Client c, Character.InventoryStateEventData inventoryData) + { + SharedWrite(msg, inventoryData.SlotRange); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 98c6a0fa9..0e1e1a839 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -32,6 +32,7 @@ namespace Barotrauma.Items.Components Drop(false, null); item.SetTransform(simPosition, 0.0f, findNewHull: false); AttachToWall(); + OnUsed.Invoke(new ItemUseInfo(item, c.Character)); item.CreateServerEvent(this); c.Character.Inventory?.CreateNetworkEvent(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 188721aa1..c1f24ccf6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -49,21 +49,35 @@ namespace Barotrauma.Items.Components msg.WriteSingle(jointAxis.Y); if (StickTarget.UserData is Structure structure) { + msg.WriteByte((byte)StickTargetType.Structure); msg.WriteUInt16(structure.ID); int bodyIndex = structure.Bodies.IndexOf(StickTarget); msg.WriteByte((byte)(bodyIndex == -1 ? 0 : bodyIndex)); } - else if (StickTarget.UserData is Entity entity) + else if (StickTarget.UserData is Item item) { - msg.WriteUInt16(entity.ID); + msg.WriteByte((byte)StickTargetType.Item); + msg.WriteUInt16(item.ID); + } + else if (StickTarget.UserData is Submarine sub) + { + msg.WriteByte((byte)StickTargetType.Submarine); + msg.WriteUInt16(sub.ID); } else if (StickTarget.UserData is Limb limb) { + msg.WriteByte((byte)StickTargetType.Limb); msg.WriteUInt16(limb.character.ID); msg.WriteByte((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb)); } + else if (StickTarget.UserData is Voronoi2.VoronoiCell cell) + { + msg.WriteByte((byte)StickTargetType.LevelWall); + msg.WriteInt32(Level.Loaded.GetAllCells().IndexOf(cell)); + } else { + msg.WriteByte((byte)StickTargetType.Unknown); throw new NotImplementedException(StickTarget.UserData?.ToString() ?? "null" + " is not a valid projectile stick target."); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index e4e5845c9..089fc03df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -45,8 +45,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteSingle(deteriorationTimer); - msg.WriteSingle(deteriorateAlwaysResetTimer); - msg.WriteBoolean(DeteriorateAlways); + msg.WriteSingle(ForceDeteriorationTimer); msg.WriteSingle(tinkeringDuration); msg.WriteSingle(tinkeringStrength); msg.WriteBoolean(tinkeringPowersDevices); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs new file mode 100644 index 000000000..010111c87 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -0,0 +1,320 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal sealed partial class CircuitBox + { + /// + /// If the server needs to initialize the circuit box to the clients + /// instead of the clients loading it from the save file. + /// + private bool needsServerInitialization; + + /// + /// When in multiplayer and the circuit box is loaded from the players inventory, + /// We only load the components from XML on server side since only the server has access to CharacterCampaignData + /// and then send a network event syncing the loaded properties. But circuit box properties are too complex to + /// sync using the existing syncing logic so we instead send the state using . + /// + public void MarkServerRequiredInitialization() + => needsServerInitialization = true; + + public partial void OnDeselected(Character c) + { + ClearAllSelectionsInternal(c.ID); + BroadcastSelectionStatus(); + } + + public void ServerRead(INetSerializableStruct data, Client c) + { + switch (data) + { + case NetCircuitBoxCursorInfo { RecordedPositions.Length: 10 } cursorInfo: + { + RelayCursorState(cursorInfo, c); + break; + } + } + } + + private void RelayCursorState(NetCircuitBoxCursorInfo data, Client sender) + { + if (GameMain.Server is null || !IsRoundRunning()) { return; } + + SendToAll(CircuitBoxOpcode.Cursor, data with { CharacterID = sender.CharacterID }, FilterClients); + + bool FilterClients(Client client) + { + // ReSharper disable once RedundantAssignment + bool isSender = client == sender; +#if DEBUG + // Shown own cursor in debug builds + isSender = false; +#endif + return !isSender && client.Character is not null && client.Character.SelectedItem == item; + } + } + + public void SendToClient(CircuitBoxOpcode opcode, INetSerializableStruct data, Client targetClient) + { + var (msg, deliveryMethod) = PrepareToSend(opcode, data); + + GameMain.Server?.ServerPeer?.Send(msg, targetClient.Connection, deliveryMethod); + } + + public void SendToAll(CircuitBoxOpcode opcode, INetSerializableStruct data, Func? predicate = null) + { + var (msg, deliveryMethod) = PrepareToSend(opcode, data); + + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (predicate is not null && !predicate(client)) { continue; } + + GameMain.Server?.ServerPeer?.Send(msg, client.Connection, deliveryMethod); + } + } + + private (IWriteMessage Message, DeliveryMethod DeliveryMethod) PrepareToSend(CircuitBoxOpcode opcode, INetSerializableStruct data) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.CIRCUITBOX); + + msg.WriteNetSerializableStruct(new NetCircuitBoxHeader( + Opcode: opcode, + ItemID: item.ID, + ComponentIndex: (byte)item.GetComponentIndex(this))); + + msg.WriteNetSerializableStruct(data); + + DeliveryMethod deliveryMethod = + UnrealiableOpcodes.Contains(opcode) + ? DeliveryMethod.Unreliable + : DeliveryMethod.Reliable; + + return (msg, deliveryMethod); + } + + public void CreateServerEvent(INetSerializableStruct data) + => item.CreateServerEvent(this, new CircuitBoxEventData(data)); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) + { + if (extraData is null) { return; } + + var eventData = ExtractEventData(extraData); + msg.WriteByte((byte)eventData.Opcode); + msg.WriteNetSerializableStruct(eventData.Data); + } + + public void ServerEventRead(IReadMessage msg, Client c) + { + var header = (CircuitBoxOpcode)msg.ReadByte(); + switch (header) + { + case CircuitBoxOpcode.AddComponent: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.PrefabIdentifier); + if (prefab is null) + { + ThrowError("Unable to add component because the prefab was not found.", c); + return; + } + + if (IsFull || !GetApplicableResourcePlayerHas(prefab, c.Character).TryUnwrap(out var resource)) { return; } + + ushort id = ICircuitBoxIdentifiable.FindFreeID(Components); + if (id is ICircuitBoxIdentifiable.NullComponentID) + { + ThrowError("Unable to add component because there are no available IDs left.", c); + return; + } + + bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, it => + { + CreateServerEvent(new CircuitBoxServerCreateComponentEvent(it.ID, resource.Prefab.UintIdentifier, id, data.Position)); + }); + + if (!result) + { + ThrowError("Unable to add component because the component could not be created.", c); + return; + } + + GameServer.Log($"{NetworkMember.ClientLogName(c)} added a {prefab.Name} into a circuit box.", ServerLog.MessageType.Wiring); + RemoveItem(resource); + break; + } + case CircuitBoxOpcode.MoveComponent: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.DeleteComponent: + { + var data = INetSerializableStruct.Read(msg); + if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + + CreateRefundItemsForUsedResources(data.TargetIDs, c.Character); + GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogComponentName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); + RemoveComponentInternal(data.TargetIDs); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.SelectComponents: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + SelectComponentsInternal(data.TargetIDs, c.CharacterID, data.Overwrite); + SelectInputOutputInternal(data.IOs, c.CharacterID, data.Overwrite); + BroadcastSelectionStatus(); + break; + } + case CircuitBoxOpcode.SelectWires: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + SelectWiresInternal(data.TargetIDs, c.CharacterID, data.Overwrite); + BroadcastSelectionStatus(); + break; + } + case CircuitBoxOpcode.AddWire: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.SelectedWirePrefabIdentifier); + if (prefab is null) + { + ThrowError($"Unable to connect wire because wire by identifier \"{data.SelectedWirePrefabIdentifier}\" was not found.", c); + break; + } + + if (data.Start.FindConnection(this).TryUnwrap(out var start) && + data.End.FindConnection(this).TryUnwrap(out var end)) + { + bool result = Connect(start, end, wire => + { + CreateServerEvent(new CircuitBoxServerCreateWireEvent(data with { Start = wire.Start, End = wire.End }, wire.ID, wire.Item.Select(static i => i.ID))); + }, prefab); + + if (!result) + { + ThrowError("Unable to connect wire because the circuit box rejected it.", c); + } + + GameServer.Log($"{NetworkMember.ClientLogName(c)} connected a wire from {start.Name} to {end.Name} in a circuit box.", ServerLog.MessageType.Wiring); + } + else + { + ThrowError($"Unable to connect wire because the start or end connection was not found. (start: {data.Start}, end: {data.End})", c); + } + + break; + } + case CircuitBoxOpcode.RemoveWire: + { + var data = INetSerializableStruct.Read(msg); + if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + + GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogWireName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); + RemoveWireInternal(data.TargetIDs); + CreateServerEvent(data); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); + } + + string GetLogComponentName(IReadOnlyList ids) + { + if (ids.Count > 1) { return $"{ids.Count} components"; } + return Components.FirstOrDefault(comp => ids.Contains(comp.ID))?.Item.Name ?? "[UNKNOWN]"; + } + + string GetLogWireName(IReadOnlyList ids) + { + if (ids.Count > 1) { return $"{ids.Count} wires"; } + if (Wires.FirstOrDefault(w => ids.Contains(w.ID)) is not { } wire) { return "[UNKNOWN]"; } + + return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire"; + } + } + + /// + /// Creates an event that overrides the state of the circuit box for all clients. + /// This is only required if the circuit box is loaded from the players inventory in multiplayer. + /// + public void CreateInitializationEvent() + { + Vector2 inputPos = Vector2.Zero, + outputPos = Vector2.Zero; + + foreach (var ioNode in InputOutputNodes) + { + switch (ioNode.NodeType) + { + case CircuitBoxInputOutputNode.Type.Input: + inputPos = ioNode.Position; + break; + case CircuitBoxInputOutputNode.Type.Output: + outputPos = ioNode.Position; + break; + } + } + + CircuitBoxInitializeStateFromServerEvent data = new( + Components: Components.Select(EventFromComponent).ToImmutableArray(), + Wires: Wires.Select(EventFromWire).ToImmutableArray(), + InputPos: inputPos, + OutputPos: outputPos); + + CreateServerEvent(data); + + static CircuitBoxServerCreateComponentEvent EventFromComponent(CircuitBoxComponent component) + => new(component.Item.ID, component.UsedResource.UintIdentifier, component.ID, component.Position); + + static CircuitBoxServerCreateWireEvent EventFromWire(CircuitBoxWire wire) + { + var backingWire = wire.BackingWire.Select(static i => i.ID); + var from = CircuitBoxConnectorIdentifier.FromConnection(wire.From); + var to = CircuitBoxConnectorIdentifier.FromConnection(wire.To); + + var request = new CircuitBoxClientAddWireEvent(wire.Color, from, to, wire.UsedItemPrefab.UintIdentifier); + return new CircuitBoxServerCreateWireEvent(request, wire.ID, backingWire); + } + } + + // we don't care about updating the view on server + public partial void OnViewUpdateProjSpecific() { } + + private void ThrowError(string message, Client c) + { + DebugConsole.ThrowError(message); + SendToClient(CircuitBoxOpcode.Error, new CircuitBoxErrorEvent(message), c); + } + + private void BroadcastSelectionStatus() + { + var nodes = Components.Select(static c => new CircuitBoxIdSelectionPair(c.ID, c.IsSelected ? Option.Some(c.SelectedBy) : Option.None)).ToImmutableArray(); + var wires = Wires.Select(static w => new CircuitBoxIdSelectionPair(w.ID, w.IsSelected ? Option.Some(w.SelectedBy) : Option.None)).ToImmutableArray(); + var ios = InputOutputNodes.Select(static n => new CircuitBoxTypeSelectionPair(n.NodeType, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); + + CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios)); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 9403732da..3370c6afd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -12,7 +12,8 @@ namespace Barotrauma.Items.Components List[] wires = new List[Connections.Count]; //read wire IDs for each connection - for (int i = 0; i < Connections.Count; i++) + byte connectionCount = msg.ReadByte(); + for (int i = 0; i < Connections.Count && i < connectionCount; i++) { wires[i] = new List(); uint wireCount = msg.ReadVariableUInt32(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index fd03a0bfd..fb2736c41 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -32,17 +32,17 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; - ShowOnDisplay(newOutputValue, addToHistory: true, TextColor); + ShowOnDisplay(newOutputValue, addToHistory: true, TextColor, isWelcomeMessage: false); item.SendSignal(newOutputValue, "signal_out"); item.CreateServerEvent(this); } } - partial void ShowOnDisplay(string input, bool addToHistory, Color color) + partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage) { if (addToHistory) { - messageHistory.Add(new TerminalMessage(input, color)); + messageHistory.Add(new TerminalMessage(input, color, isWelcomeMessage)); while (messageHistory.Count > MaxMessages) { messageHistory.RemoveAt(0); @@ -54,9 +54,11 @@ namespace Barotrauma.Items.Components { //split too long messages to multiple parts int msgIndex = 0; - foreach (var (str, _) in messageHistory) + foreach (var msg in messageHistory) { - string msgToSend = str; + //the clients create the welcome message themselves, no need to sync it + if (msg.IsWelcomeMessage) { continue; } + string msgToSend = msg.Text; if (string.IsNullOrEmpty(msgToSend)) { item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 84c67ceae..a063286d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -4,17 +4,25 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { - partial class Inventory : IServerSerializable, IClientSerializable + partial class Inventory : IClientSerializable { + private readonly Dictionary[]> receivedItemIds = new Dictionary[]>(); + public void ServerEventRead(IReadMessage msg, Client c) { List prevItems = new List(AllItems.Distinct()); - SharedRead(msg, out var newItemIDs); + if (!receivedItemIds.TryGetValue(c, out List[] receivedItemIdsFromClient)) + { + receivedItemIdsFromClient = new List[capacity]; + receivedItemIds.Add(c, receivedItemIdsFromClient); + } + + SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply); + if (!readyToApply) { return; } if (c == null || c.Character == null) { return; } @@ -39,7 +47,7 @@ namespace Barotrauma CreateNetworkEvent(); for (int i = 0; i < capacity; i++) { - foreach (ushort id in newItemIDs[i]) + foreach (ushort id in receivedItemIdsFromClient[i]) { if (Entity.FindEntityByID(id) is not Item item) { continue; } item.PositionUpdateInterval = 0.0f; @@ -58,7 +66,7 @@ namespace Barotrauma { foreach (Item item in slots[i].Items.ToList()) { - if (!newItemIDs[i].Contains(item.ID)) + if (!receivedItemIdsFromClient[i].Contains(item.ID)) { Item droppedItem = item; Entity prevOwner = Owner; @@ -85,7 +93,7 @@ namespace Barotrauma } } - foreach (ushort id in newItemIDs[i]) + foreach (ushort id in receivedItemIdsFromClient[i]) { Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item; prevItemInventories.Add(newItem?.ParentInventory); @@ -94,7 +102,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - foreach (ushort id in newItemIDs[i]) + foreach (ushort id in receivedItemIdsFromClient[i]) { if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } @@ -128,7 +136,7 @@ namespace Barotrauma TryPutItem(item, i, true, true, c.Character, false); for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(item) && !newItemIDs[j].Contains(item.ID)) + if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) { slots[j].RemoveItem(item); } @@ -138,42 +146,48 @@ namespace Barotrauma EnsureItemsInBothHands(c.Character); + receivedItemIds.Remove(c); + CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } - foreach (Item item in AllItems.Distinct()) + foreach (Item item in AllItems.DistinctBy(it => it.Prefab)) { if (item == null) { continue; } if (!prevItems.Contains(item)) { + int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it)); + string amountText = amount > 1 ? $"x{amount} " : string.Empty; if (Owner == c.Character) { HumanAIController.ItemTaken(item, c.Character); - GameServer.Log(GameServer.CharacterLogName(c.Character) + " picked up " + item.Name, ServerLog.MessageType.Inventory); + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory); } else { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory); + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} placed {amountText}{item.Name} in {Owner}", ServerLog.MessageType.Inventory); } } } - foreach (Item item in prevItems.Distinct()) + + var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it)); + foreach (Item item in droppedItems.DistinctBy(it => it.Prefab)) { - if (item == null) { continue; } - if (!AllItems.Contains(item)) + var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it)); + int amount = matchingItems.Count(); + string amountText = amount > 1 ? $"x{amount} " : string.Empty; + if (Owner == c.Character) { - if (Owner == c.Character) - { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + item.Name, ServerLog.MessageType.Inventory); - } - else - { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory); - } + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory); } + else + { + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} removed {amountText}{item.Name} from {Owner}", ServerLog.MessageType.Inventory); + } + item.CreateDroppedStack(matchingItems, allowClientExecute: true); } } @@ -203,10 +217,5 @@ namespace Barotrauma bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length; } - - public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) - { - SharedWrite(msg, extraData); - } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index a48f13895..f59311120 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -18,9 +19,23 @@ namespace Barotrauma get { return base.Prefab?.Sprite; } } - partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) + private readonly Dictionary campaignInteractionTypePerClient = new Dictionary(); + + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients) { - GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData()); + if (Removed) { return; } + if (targetClients == null || targetClients.None()) + { + campaignInteractionTypePerClient.Clear(); + } + else + { + foreach (Client client in targetClients) + { + campaignInteractionTypePerClient[client] = interactionType; + } + } + GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData(targetClients)); } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) @@ -33,7 +48,7 @@ namespace Barotrauma } if (extraData is null) { throw error("event data was null"); } - if (!(extraData is IEventData itemEventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } + if (extraData is not IEventData itemEventData) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } msg.WriteRangedInteger((int)itemEventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); switch (itemEventData) @@ -44,7 +59,7 @@ namespace Barotrauma { throw error($"component index out of range ({componentIndex})"); } - if (!(components[componentIndex] is IServerSerializable serializableComponent)) + if (components[componentIndex] is not IServerSerializable serializableComponent) { throw error($"component \"{components[componentIndex]}\" is not server serializable"); } @@ -57,20 +72,28 @@ namespace Barotrauma { throw error($"container index out of range ({containerIndex})"); } - if (!(components[containerIndex] is ItemContainer itemContainer)) + if (components[containerIndex] is not ItemContainer itemContainer) { throw error("component \"" + components[containerIndex] + "\" is not server serializable"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - itemContainer.Inventory.ServerEventWrite(msg, c); + itemContainer.Inventory.ServerEventWrite(msg, c, inventoryStateEventData); break; case ItemStatusEventData statusEvent: msg.WriteBoolean(statusEvent.LoadingRound); msg.WriteSingle(condition); break; - case AssignCampaignInteractionEventData _: - msg.WriteByte((byte)CampaignInteractionType); + case AssignCampaignInteractionEventData campaignInteractionData: + bool isVisibleToClient = + campaignInteractionData.TargetClients == null || + campaignInteractionData.TargetClients.IsEmpty || + campaignInteractionData.TargetClients.Contains(c); + msg.WriteBoolean(isVisibleToClient); + if (isVisibleToClient) + { + msg.WriteByte((byte)CampaignInteractionType); + } break; case ApplyStatusEffectEventData applyStatusEffectEventData: { @@ -131,6 +154,13 @@ namespace Barotrauma } } break; + case DroppedStackEventData droppedStackEventData: + msg.WriteRangedInteger(droppedStackEventData.Items.Length, 0, Inventory.MaxPossibleStackSize); + foreach (Item droppedItem in droppedStackEventData.Items) + { + msg.WriteUInt16(droppedItem.ID); + } + break; default: throw error($"Unsupported event type {itemEventData.GetType().Name}"); } @@ -257,10 +287,10 @@ namespace Barotrauma msg.WriteInt32(idCardComponent.SubmarineSpecificID); msg.WriteString(idCardComponent.OwnerName); msg.WriteString(idCardComponent.OwnerTags); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1)); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1)); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex + 1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex + 1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex + 1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex + 1)); msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor); msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor); msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs new file mode 100644 index 000000000..b2a6b0dda --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma; + +partial class Item +{ + private readonly struct DroppedStackEventData : IEventData + { + public EventType EventType => EventType.DroppedStack; + public readonly ImmutableArray Items; + + public DroppedStackEventData(IEnumerable items) + { + Items = items.Distinct().ToImmutableArray(); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs new file mode 100644 index 000000000..0c87880d4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs @@ -0,0 +1,13 @@ +using Barotrauma.Networking; +using System; + +namespace Barotrauma +{ + partial class ItemInventory : Inventory + { + public void ServerEventWrite(IWriteMessage msg, Client c, Item.InventoryStateEventData inventoryData) + { + SharedWrite(msg, inventoryData.SlotRange); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 74ca52bb7..bdd772286 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -176,6 +176,7 @@ namespace Barotrauma BackgroundSections[i].SetColor(color); }, out int sectorToUpdate); + RefreshAveragePaintedColor(); //add to pending updates to notify other clients as well pendingSectionUpdates.Add(sectorToUpdate); break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index c6d6559cc..f8f8d2bbd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -42,8 +42,6 @@ namespace Barotrauma.Networking public string RejectedName; - public int RoundsSincePlayedAsTraitor; - public float KickAFKTimer; public double MidRoundSyncTimeOut; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 318f77ff8..0535add38 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Networking { await Task.Yield(); string dir = mod.Dir; - SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod), fileName => { }); + SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod)); } private void DeleteDir() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d10033fc6..34f6cd497 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -74,13 +74,21 @@ namespace Barotrauma.Networking private bool initiatedStartGame; private CoroutineHandle startGameCoroutine; - public TraitorManager TraitorManager; - private readonly ServerEntityEventManager entityEventManager; public FileSender FileSender { get; private set; } public ModSender ModSender { get; private set; } + + private TraitorManager traitorManager; + public TraitorManager TraitorManager + { + get + { + traitorManager ??= new TraitorManager(this); + return traitorManager; + } + } #if DEBUG public void PrintSenderTransters() @@ -358,22 +366,31 @@ namespace Barotrauma.Networking for (int i = Character.CharacterList.Count - 1; i >= 0; i--) { Character character = Character.CharacterList[i]; - if (character.IsDead || !character.ClientDisconnected) { continue; } - - character.KillDisconnectedTimer += deltaTime; - character.SetStun(1.0f); + if (!character.ClientDisconnected) { continue; } Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); - if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) + if (!character.IsDead) { - character.Kill(CauseOfDeathType.Disconnected, null); - continue; - } + character.KillDisconnectedTimer += deltaTime; + character.SetStun(1.0f); - if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) + if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) + { + character.Kill(CauseOfDeathType.Disconnected, null); + continue; + } + if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && + (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) + { + SetClientCharacter(owner, character); + } + } + else if (owner != null && + character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected && + character.CharacterHealth.VitalityDisregardingDeath > 0) { SetClientCharacter(owner, character); + character.Revive(removeAfflictions: false); } } @@ -412,12 +429,7 @@ namespace Barotrauma.Networking } float endRoundDelay = 1.0f; - if (TraitorManager?.ShouldEndRound ?? false) - { - endRoundDelay = 5.0f; - endRoundTimer += deltaTime; - } - else if (ServerSettings.AutoRestart && isCrewDead) + if (ServerSettings.AutoRestart && isCrewDead) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; @@ -452,11 +464,7 @@ namespace Barotrauma.Networking if (endRoundTimer >= endRoundDelay) { - if (TraitorManager?.ShouldEndRound ?? false) - { - Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage); - } - else if (ServerSettings.AutoRestart && isCrewDead) + if (ServerSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } @@ -830,6 +838,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.MEDICAL: ReadMedicalMessage(inc, connectedClient); break; + case ClientPacketHeader.CIRCUITBOX: + ReadCircuitBoxMessage(inc, connectedClient); + break; case ClientPacketHeader.READY_CHECK: ReadyCheck.ServerRead(inc, connectedClient); break; @@ -1299,6 +1310,22 @@ namespace Barotrauma.Networking } } + private static void ReadCircuitBoxMessage(IReadMessage inc, Client sender) + { + var header = INetSerializableStruct.Read(inc); + + INetSerializableStruct data = header.Opcode switch + { + CircuitBoxOpcode.Cursor => INetSerializableStruct.Read(inc), + _ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.") + }; + + if (header.FindTarget().TryUnwrap(out var box)) + { + box.ServerRead(data, sender); + } + } + private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { sender.SpectateOnly = inc.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); @@ -1775,7 +1802,7 @@ namespace Barotrauma.Networking while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) { var entity = c.PendingPositionUpdates.Peek(); - if (!(entity is IServerPositionSync entityPositionSync) || + if (entity is not IServerPositionSync entityPositionSync || entity.Removed || (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { @@ -1957,7 +1984,8 @@ namespace Barotrauma.Networking outmsg.WriteBoolean(ServerSettings.AllowSpectating); - outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); + outmsg.WriteSingle(ServerSettings.TraitorProbability); + outmsg.WriteRangedInteger(ServerSettings.TraitorDangerLevel, TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); @@ -2212,6 +2240,7 @@ namespace Barotrauma.Networking //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { + traitorManager = new TraitorManager(this); GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType); } else @@ -2509,16 +2538,8 @@ namespace Barotrauma.Networking } } - TraitorManager = null; - if (ServerSettings.TraitorsEnabled == YesNoMaybe.Yes || - (ServerSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) - { - if (!(GameMain.GameSession?.GameMode is CampaignMode)) - { - TraitorManager = new TraitorManager(); - TraitorManager.Start(this); - } - } + TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded); + TraitorManager.Enabled = Rand.Range(0.0f, 1.0f) < ServerSettings.TraitorProbability; GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled")); @@ -2562,7 +2583,6 @@ namespace Barotrauma.Networking msg.WriteBoolean(ServerSettings.AllowRewiring); msg.WriteBoolean(ServerSettings.AllowFriendlyFire); msg.WriteBoolean(ServerSettings.LockAllDefaultWires); - msg.WriteBoolean(ServerSettings.AllowRagdollButton); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteBoolean(IsUsingRespawnShuttle()); @@ -2685,13 +2705,12 @@ namespace Barotrauma.Networking } string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); - var traitorResults = TraitorManager?.GetEndResults() ?? new List(); - List missions = GameMain.GameSession.Missions.ToList(); if (GameMain.GameSession is { IsRunning: true }) { - GameMain.GameSession.EndRound(endMessage, traitorResults); + GameMain.GameSession.EndRound(endMessage); } + TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null; endRoundTimer = 0.0f; @@ -2736,10 +2755,10 @@ namespace Barotrauma.Networking } msg.WriteByte(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); - msg.WriteByte((byte)traitorResults.Count); - foreach (var traitorResult in traitorResults) + msg.WriteBoolean(traitorResults.HasValue); + if (traitorResults.HasValue) { - traitorResult.ServerWrite(msg); + msg.WriteNetSerializableStruct(traitorResults.Value); } foreach (Client client in connectedClients) @@ -2788,13 +2807,14 @@ namespace Barotrauma.Networking if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; } var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime; - if (timeSinceNameChange < Client.NameChangeCoolDown) + if (timeSinceNameChange < Client.NameChangeCoolDown && newName != c.Name) { //only send once per second at most to prevent using this for spamming if (timeSinceNameChange.TotalSeconds > 1) { var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); + LastClientListUpdateID++; } c.NameId = nameId; c.RejectedName = newName; @@ -3558,14 +3578,9 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void SendTraitorMessage(Client client, string message, Identifier missionIdentifier, TraitorMessageType messageType) + public void SendTraitorMessage(WriteOnlyMessage msg, Client client) { - if (client == null) { return; } - var msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE); - msg.WriteByte((byte)messageType); - msg.WriteIdentifier(missionIdentifier); - msg.WriteString(message); + if (client == null) { return; }; serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 737e9555d..8c0fceec9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -240,19 +240,20 @@ namespace Barotrauma { Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == inventory.Owner); - Character yoinkerCharacter = yoinker?.Character; + Character thiefCharacter = yoinker?.Character; Character targetCharacter = inventory.Owner as Character; - if (yoinker == null || item == null || yoinkerCharacter == null || targetCharacter == null || yoinkerCharacter == targetCharacter) { return; } + if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; } if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; } // Only if the target is alive and they are stunned, unconscious or handcuffed if (targetCharacter.IsDead || targetCharacter.Removed || !(targetCharacter.Stun > 0) && !targetCharacter.IsUnconscious && !targetCharacter.LockHands) { return; } - if (GameMain.Server.TraitorManager?.Traitors != null) + if (GameMain.Server.TraitorManager != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == targetCharacter || t.Character == yoinkerCharacter)) + if (GameMain.Server.TraitorManager.IsTraitor(targetCharacter) || + GameMain.Server.TraitorManager.IsTraitor(thiefCharacter)) { // Don't penalize traitors return; @@ -299,7 +300,7 @@ namespace Barotrauma } // Name tag doesn't belong to anyone in particular or we own the ID card - if (name == null || name == yoinkerCharacter.Name) { return; } + if (name == null || name == thiefCharacter.Name) { return; } } if (MathUtils.NearlyEqual(DangerousItemStealKarmaDecrease, 0)) { return; } @@ -329,7 +330,7 @@ namespace Barotrauma karmaDecrease *= 0.5f; } - AdjustKarma(yoinkerCharacter, -karmaDecrease, "Stolen dangerous item"); + AdjustKarma(thiefCharacter, -karmaDecrease, "Stolen dangerous item"); } public void OnCharacterHealthChanged(Character target, Character attacker, float damage, float stun, IEnumerable appliedAfflictions = null) @@ -341,27 +342,23 @@ namespace Barotrauma if (target.IsDead || target.Removed) { return; } bool isEnemy = target.AIController is EnemyAIController || target.TeamID != attacker.TeamID; - if (GameMain.Server.TraitorManager?.Traitors != null) + if (GameMain.Server.TraitorManager != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == target)) + if (GameMain.Server.TraitorManager.IsTraitor(target)) { //traitors always count as enemies isEnemy = true; } - if (GameMain.Server.TraitorManager.Traitors.Any(t => - t.Character == attacker && - t.CurrentObjective != null && - t.CurrentObjective.IsEnemy(target))) + if (GameMain.Server.TraitorManager.IsTraitor(attacker)) { - //target counts as an enemy to the traitor + //others count as an enemies to the traitor isEnemy = true; } } - bool targetIsHusk = target.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; - bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; + static bool IsHusk(Character c) => c.IsHusk || c.IsHuskInfected; //huskified characters count as enemies to healthy characters and vice versa - if (targetIsHusk != attackerIsHusk) { isEnemy = true; } + if (IsHusk(attacker) != IsHusk(target)) { isEnemy = true; } if (appliedAfflictions != null) { @@ -478,14 +475,11 @@ namespace Barotrauma if (damageAmount > 0) { if (StructureDamageKarmaDecrease <= 0.0f) { return; } - if (GameMain.Server.TraitorManager?.Traitors != null) + if (GameMain.Server.TraitorManager != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => - t.Character == attacker && - t.CurrentObjective != null && - t.CurrentObjective.IsAllowedToDamage(structure))) + if (GameMain.Server.TraitorManager.IsTraitor(attacker)) { - //traitor tasked to flood the sub -> damaging structures is ok + //traitors are allowed to damage structures return; } } @@ -580,7 +574,7 @@ namespace Barotrauma public void OnItemContained(Item containedItem, Item container, Character character) { if (containedItem == null || container == null || character == null || character.IsTraitor) { return; } - if (container.Prefab.Identifier == "weldingtool" && containedItem.HasTag("oxygensource")) + if (container.Prefab.Identifier == Tags.WeldingFuel && containedItem.HasTag(Tags.OxygenSource)) { var client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (client == null) { return; } @@ -614,7 +608,7 @@ namespace Barotrauma if (amount < 0.0f) { - float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); var clientMemory = GetClientMemory(client); clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index b60d34661..eaaf5e1dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Networking AutoExpandMTU = false, MaximumConnections = NetConfig.MaxPlayers * 2, EnableUPnP = serverSettings.EnableUPnP, - Port = serverSettings.Port + Port = serverSettings.Port, + DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; netPeerConfiguration.DisableMessageType( diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index bc268f9dc..187f0f1af 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -433,9 +433,9 @@ namespace Barotrauma.Networking } //tell the respawning client they're no longer a traitor - if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null) + if (GameMain.Server.TraitorManager != null && clients[i].Character != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == clients[i].Character)) + if (GameMain.Server.TraitorManager.IsTraitor(clients[i].Character)) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("TraitorRespawnMessage"), clients[i], ChatMessageType.ServerMessageBox); } @@ -537,18 +537,7 @@ namespace Barotrauma.Networking } //add the ID card tags they should've gotten when spawning in the shuttle - foreach (Item item in character.Inventory.AllItems.Distinct()) - { - if (item.GetComponent() == null) { continue; } - foreach (string s in shuttleSpawnPoints[i].IdCardTags) - { - item.AddTag(s); - } - if (!string.IsNullOrWhiteSpace(shuttleSpawnPoints[i].IdCardDesc)) - { - item.Description = shuttleSpawnPoints[i].IdCardDesc; - } - } + character.GiveIdCardTags(shuttleSpawnPoints[i], requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); } } @@ -559,7 +548,7 @@ namespace Barotrauma.Networking { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath); + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillLossPercentageOnDeath / 100.0f); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 185b5aad5..ec0e8e101 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -215,11 +215,15 @@ namespace Barotrauma.Networking int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); - - int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1; - if (traitorSetting < 0) { traitorSetting = 2; } - if (traitorSetting > 2) { traitorSetting = 0; } - TraitorsEnabled = (YesNoMaybe)traitorSetting; + + bool changedTraitorProbability = incMsg.ReadBoolean(); + float traitorProbability = incMsg.ReadSingle(); + if (changedTraitorProbability) + { + TraitorProbability = traitorProbability; + } + //the byte indicates the direction we're changing the value, subtract one to get negative values from a byte + TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1; int botCount = BotCount + incMsg.ReadByte() - 1; if (botCount < 0) { botCount = MaxBotCount; } @@ -347,7 +351,7 @@ namespace Barotrauma.Networking selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); - GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); if (HiddenSubs.Any()) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index c6520b8fa..5afb8d321 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -337,6 +337,20 @@ namespace Barotrauma sender.SetVote(voteType, (int)inc.ReadByte()); } break; + case VoteType.Traitor: + int clientId = inc.ReadInt32(); + if (sender.InGame && sender.Character != null) + { + var client = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.SessionId == clientId); + sender.SetVote(voteType, client); + if (client?.Character != null) + { + GameMain.Server.SendChatMessage( + TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value, + ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character); + } + } + break; } inc.ReadPadBits(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 56b57530c..e0967195b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Globalization; +using System.Linq; using Barotrauma.Networking; namespace Barotrauma.Steam @@ -71,7 +72,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); Steamworks.SteamServer.SetKey("allowspectating", server.ServerSettings.AllowSpectating.ToString()); Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString()); - Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); + Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture)); Steamworks.SteamServer.SetKey("friendlyfireenabled", server.ServerSettings.AllowFriendlyFire.ToString()); Steamworks.SteamServer.SetKey("karmaenabled", server.ServerSettings.KarmaEnabled.ToString()); Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs deleted file mode 100644 index 37894b947..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; -using Microsoft.SqlServer.Server; - -namespace Barotrauma -{ - partial class Traitor - { - public abstract class Goal - { - public HashSet Traitors { get; } = new HashSet(); - public TraitorMission Mission { get; internal set; } - - public virtual string StatusTextId { get; set; } = "TraitorGoalStatusTextFormat"; - - public virtual string InfoTextId { get; set; } = null; - - public virtual string CompletedTextId { get; set; } = null; - - public virtual string StatusValueTextId => IsCompleted ? "complete" : "inprogress"; - - public virtual IEnumerable StatusTextKeys => new [] { "[infotext]", "[status]" }; - public virtual IEnumerable StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; - - public virtual IEnumerable InfoTextKeys => new string[] { }; - public virtual IEnumerable InfoTextValues(Traitor traitor) => new string[] { }; - - public virtual IEnumerable CompletedTextKeys => new string[] { }; - public virtual IEnumerable CompletedTextValues(Traitor traitor) => new string[] { }; - - protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - => TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, textId, keys.Zip(values, (k,v) => (k,v)).ToArray()); - - protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - protected internal virtual string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - - public virtual string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor)); - public virtual string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor)); - - public virtual string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor); - - public abstract bool IsCompleted { get; } - public virtual bool IsStarted(Traitor traitor) => Traitors.Contains(traitor); - public virtual bool CanBeCompleted(ICollection traitors) => !Traitors.Any(traitor => traitor.Character?.IsDead ?? true); - public virtual bool IsEnemy(Character character) => false; - public virtual bool IsAllowedToDamage(Structure structure) => false; - public virtual bool Start(Traitor traitor) - { - Traitors.Add(traitor); - return true; - } - - public virtual void Update(float deltaTime) - { - } - - protected Goal() - { - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs deleted file mode 100644 index 6fd116862..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalDestroyItemsWithTag : Goal - { - private readonly string tag; - private readonly bool matchIdentifier; - private readonly bool matchTag; - private readonly bool matchInventory; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]", "[tag]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" }); - - private readonly float destroyPercent; - private float DestroyPercent => destroyPercent; - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private int totalCount = 0; - private int targetCount = 0; - private string tagPrefabName = null; - - private int CountMatchingItems() - { - int result = 0; - foreach (var item in Item.ItemList) - { - if (!matchInventory && Traitors.All(traitor => item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != traitor.Character) != null)) - { - continue; - } - - if (item.Submarine == null) - { - //items outside the sub don't count as destroyed if they're still in the traitor's inventory - bool carriedByTraitor = Traitors.Any(traitor => item.IsOwnedBy(traitor.Character)); - if (!carriedByTraitor) { continue; } - } - else - { - if (Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; } - } - - if (item.Condition <= 0.0f) - { - continue; - } - var identifierMatches = matchIdentifier && ((MapEntity)item).Prefab.Identifier == tag; - if (identifierMatches && tagPrefabName == null) - { - var textId = item.Prefab.GetItemNameTextId(); - tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name.Value; - } - if (identifierMatches || (matchTag && item.HasTag(tag))) - { - ++result; - } - } - - // Quick fix - if (tagPrefabName == null && matchIdentifier) - { - tagPrefabName = TextManager.FormatServerMessage($"entityname.{tag}"); - } - - return result; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = CountMatchingItems() <= targetCount; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - totalCount = CountMatchingItems(); - if (totalCount <= 0) - { - return false; - } - targetCount = (int)((1.0f - destroyPercent) * totalCount - 0.5f); - return true; - } - - public GoalDestroyItemsWithTag(string tag, float destroyPercent, bool matchTag, bool matchIdentifier, bool matchInventory) : base() - { - InfoTextId = "TraitorGoalDestroyItems"; - this.tag = tag; - this.destroyPercent = destroyPercent; - this.matchTag = matchTag; - this.matchIdentifier = matchIdentifier; - this.matchInventory = matchInventory; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs deleted file mode 100644 index e9977fa73..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalEntityTransformation : Goal - { - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[catalystitem]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { catalystItemName }); - - private bool isCompleted; - public override bool IsCompleted => isCompleted; - - private string catalystItemIdentifier, catalystItemName; - - private Vector2 activeEntitySavedPosition; - private Entity activeEntity; - private int activeEntityIndex; - private const float gracePeriod = 1f; - private const float graceDistance = 200f; - private float graceTimer; - private double transformationTime; - - private enum EntityTypes { Character, Item } - - private Identifier[] entities; - private EntityTypes[] entityTypes; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = HasTransformed(deltaTime); - } - - public override bool CanBeCompleted(ICollection traitors) - { - return graceTimer <= gracePeriod; - } - - private bool HasTransformed(float deltaTime) - { - if (activeEntity != null && !activeEntity.Removed) - { - activeEntitySavedPosition = activeEntity.WorldPosition; - } - else - { - if (transformationTime == 0) - { - graceTimer = 0.0f; - activeEntityIndex++; - transformationTime = Timing.TotalTime; - } - graceTimer += deltaTime; - - switch (entityTypes[activeEntityIndex]) - { - case EntityTypes.Character: - foreach (Character character in Character.CharacterList) - { - if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < transformationTime) - { - continue; - } - if (character.SpeciesName == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) - { - activeEntity = character; - transformationTime = 0.0; - return activeEntityIndex == entities.Length - 1; - } - } - break; - case EntityTypes.Item: - foreach (Item item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID) || item.SpawnTime + gracePeriod < transformationTime) - { - continue; - } - if (((MapEntity)item).Prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) - { - activeEntity = item; - transformationTime = 0.0; - return activeEntityIndex == entities.Length - 1; - } - } - break; - } - } - - return false; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - - catalystItemName = TextManager.FormatServerMessage($"entityname.{catalystItemIdentifier}"); - - activeEntity = null; - activeEntityIndex = 0; - - switch (entityTypes[activeEntityIndex]) - { - case EntityTypes.Character: - foreach (Character character in Character.CharacterList) - { - if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (character.SpeciesName == entities[activeEntityIndex]) - { - activeEntity = character; - break; - } - } - break; - case EntityTypes.Item: - foreach (Item item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (((MapEntity)item).Prefab.Identifier == entities[0]) - { - activeEntity = item; - break; - } - } - break; - } - - graceTimer = 0.0f; - return activeEntity != null; - } - - public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base() - { - this.entities = entities.ToIdentifiers().ToArray(); - - this.entityTypes = new EntityTypes[entityTypes.Length]; - - for (int i = 0; i < this.entityTypes.Length; i++) - { - this.entityTypes[i] = (EntityTypes)Enum.Parse(typeof(EntityTypes), entityTypes[i], true); - } - - this.catalystItemIdentifier = catalystItemIdentifier; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs deleted file mode 100644 index 57871e224..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public class GoalFindItem : HumanoidGoal - { - private readonly TraitorMission.CharacterFilter filter; - private readonly string identifier; - private readonly bool preferNew; - private readonly bool allowNew; - private readonly bool allowExisting; - private readonly HashSet allowedContainerIdentifiers = new HashSet(); - - private ItemPrefab targetPrefab; - private ItemPrefab containedPrefab; - private Item targetContainer; - private Item target; - private HashSet existingItems = new HashSet(); - private string targetNameText; - private string targetContainerNameText; - private string targetHullNameText; - private float percentage; - private int spawnAmount = 1; - - private const string itemContainerId = "toolbox"; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" }); - - public override bool IsCompleted => target != null && Traitors.Any(traitor => traitor.Character.HasItem(target)); - public override bool CanBeCompleted(ICollection traitors) - { - if (!base.CanBeCompleted(traitors)) - { - return false; - } - if (target == null) - { - var targetPrefabCandidate = FindItemPrefab(identifier); - return targetPrefabCandidate != null && FindTargetContainer(traitors, targetPrefabCandidate) != null; - } - if (target.Removed) - { - return false; - } - if (target.Submarine == null) - { - if (!(target.ParentInventory?.Owner is Character)) - { - return false; - } - } - else - { - if (Traitors.All(traitor => target.Submarine.TeamID != traitor.Character.TeamID)) - { - return false; - } - } - return true; - } - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (target != null && target.FindParentInventory(inventory => inventory == character.Inventory) != null); - - protected ItemPrefab FindItemPrefab(string identifier) - { - return (ItemPrefab)MapEntityPrefab.List.FirstOrDefault(prefab => prefab is ItemPrefab && prefab.Identifier == identifier); - } - - protected Item FindRandomContainer(ICollection traitors, ItemPrefab targetPrefabCandidate, bool includeNew, bool includeExisting) - { - List suitableItems = new List(); - foreach (Item item in Item.ItemList) - { - if (item.HiddenInGame || item.NonInteractable || item.NonPlayerTeamInteractable) { continue; } - if (item.Submarine == null || traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) - { - continue; - } - if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier)) - { - if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)) - { - suitableItems.Add(item); - } - } - } - - if (suitableItems.Count == 0) { return null; } - return suitableItems[TraitorManager.RandomInt(suitableItems.Count)]; - } - - protected Item FindTargetContainer(ICollection traitors, ItemPrefab targetPrefabCandidate) - { - Item result = null; - if (preferNew) - { - result = FindRandomContainer(traitors, targetPrefabCandidate, true, false); - } - if (result == null) - { - result = FindRandomContainer(traitors, targetPrefabCandidate, allowNew, allowExisting); - } - if (result == null) - { - return null; - } - if (allowNew && !result.OwnInventory.IsFull()) - { - return result; - } - if (allowExisting && result.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null) - { - return result; - } - return null; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - if (targetPrefab != null) - { - return true; - } - - string targetPrefabTextId; - - if (percentage > 0f) - { - spawnAmount = (int)Math.Floor(Character.CharacterList.FindAll(c => c.TeamID == traitor.Character.TeamID && c != traitor.Character && !c.IsDead && (filter == null || filter(c))).Count * percentage); - } - - if (spawnAmount > 1 && allowNew) - { - containedPrefab = FindItemPrefab(identifier); - targetPrefab = FindItemPrefab(itemContainerId); - - if (containedPrefab == null || targetPrefab == null) - { - return false; - } - - targetPrefabTextId = containedPrefab.GetItemNameTextId(); - } - else - { - spawnAmount = 1; - containedPrefab = null; - targetPrefab = FindItemPrefab(identifier); - - if (targetPrefab == null) - { - return false; - } - - targetPrefabTextId = targetPrefab.GetItemNameTextId(); - } - - targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name.Value; - targetContainer = FindTargetContainer(Traitors, targetPrefab); - if (targetContainer == null) - { - targetPrefab = null; - targetContainer = null; - return false; - } - var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId(); - targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name.Value; - var targetHullTextId = targetContainer.CurrentHull?.Prefab.GetHullNameTextId(); - targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName.Value ?? ""; - if (allowNew && !targetContainer.OwnInventory.IsFull()) - { - existingItems.Clear(); - foreach (var item in targetContainer.OwnInventory.AllItems.Distinct()) - { - existingItems.Add(item); - } - Entity.Spawner.AddItemToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item => - { - item.AddTag("traitormissionitem"); - }); - target = null; - } - else if (allowExisting) - { - target = targetContainer.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier); - } - else - { - targetPrefab = null; - targetContainer = null; - return false; - } - return true; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - if (target == null) - { - target = targetContainer.ContainedItems.FirstOrDefault(item => item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item)); - if (target != null) - { - if (containedPrefab != null) - { - for (int i = 0; i < spawnAmount; i++) - { - Entity.Spawner.AddItemToSpawnQueue(containedPrefab, target.OwnInventory); - } - } - existingItems.Clear(); - } - } - } - - public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params Identifier[] allowedContainerIdentifiers) - { - this.filter = filter; - this.identifier = identifier; - this.preferNew = preferNew; - this.allowNew = allowNew; - this.allowExisting = allowExisting; - this.percentage = percentage / 100f; - this.allowedContainerIdentifiers.UnionWith(allowedContainerIdentifiers); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs deleted file mode 100644 index 31aad8c96..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalFloodPercentOfSub : Goal - { - private readonly float minimumFloodingAmount; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - var validHullsCount = 0; - var floodingAmount = 0.0f; - foreach (Hull hull in Hull.HullList) - { - if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } - if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } - ++validHullsCount; - floodingAmount += hull.WaterVolume / hull.Volume; - } - if (validHullsCount > 0) - { - floodingAmount /= validHullsCount; - } - isCompleted = floodingAmount >= minimumFloodingAmount; - } - - public GoalFloodPercentOfSub(float minimumFloodingAmount) : base() - { - InfoTextId = "TraitorGoalFloodPercentOfSub"; - this.minimumFloodingAmount = minimumFloodingAmount; - } - } - - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs deleted file mode 100644 index 10cc8f56a..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalInjectTarget : Goal - { - public TraitorMission.CharacterFilter Filter { get; private set; } - public List Targets { get; private set; } - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[poison]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", poisonName }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); - - private string poisonId; - private string afflictionId; - private string poisonName; - private int targetCount; - private float targetPercentage; - private bool[] targetWasInfected; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = WereAllTargetsInfected(); - } - - private bool WereAllTargetsInfected() - { - if (targetWasInfected == null) { return false; } - - for (int i = 0; i < targetWasInfected.Length; i++) - { - if (targetWasInfected[i]) continue; - targetWasInfected[i] = Targets[i].CharacterHealth.GetAffliction(afflictionId) != null; - } - - return targetWasInfected.All(t => t == true); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - poisonName = TextManager.FormatServerMessage(poisonId) ?? poisonId; - - Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); - if (Targets == null) - { - return false; - } - targetWasInfected = new bool[Targets.Count]; - return !Targets.All(t => t.IsDead); - } - - public GoalInjectTarget(TraitorMission.CharacterFilter filter, string poisonId, string afflictionId, int targetCount, float targetPercentage) : base() - { - Filter = filter; - this.poisonId = poisonId; - this.afflictionId = afflictionId; - this.targetCount = targetCount; - this.targetPercentage = targetPercentage / 100f; - - if (this.targetPercentage < 1.0f) - { - InfoTextId = "traitorgoalpoisoninfo"; - } - else - { - InfoTextId = "traitorgoalpoisoneveryoneinfo"; - } - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs deleted file mode 100644 index 32b024770..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalKeepTransformedAlive : Goal - { - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[speciesname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetCharacterName }); - - public override bool IsCompleted => isCompleted; - private bool isCompleted; - - private const float gracePeriod = 1f; - private Identifier speciesId; - private string targetCharacterName; - private Character targetCharacter; - private float timer; - - public override bool CanBeCompleted(ICollection traitors) - { - return timer < gracePeriod || targetCharacter != null && !targetCharacter.IsDead; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - if (timer <= gracePeriod) - { - timer += deltaTime; - } - - isCompleted = targetCharacter != null && !targetCharacter.IsDead && timer >= gracePeriod; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - - var startTime = Timing.TotalTime; - - foreach (Character character in Character.CharacterList) - { - if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < startTime) - { - continue; - } - if (character.SpeciesName == speciesId) - { - targetCharacter = character; - break; - } - } - - targetCharacterName = TextManager.FormatServerMessage($"character.{speciesId}").ToLowerInvariant(); - - return targetCharacter != null; - } - - public GoalKeepTransformedAlive(Identifier speciesId) : base() - { - this.speciesId = speciesId; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs deleted file mode 100644 index 6cb4c70e6..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalKillTarget : Goal - { - public TraitorMission.CharacterFilter Filter { get; private set; } - public List Targets { get; private set; } - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] - { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath().Value, targetHull != null ? TextManager.Get($"roomname.{targetHull}").Value : string.Empty }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); - - private CauseOfDeathType requiredCauseOfDeath; - private string afflictionId; - private string targetHull; - private int targetCount; - private float targetPercentage; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = DoesDeathMatchCriteria(); - } - - private bool DoesDeathMatchCriteria() - { - if (Targets == null || Targets.Any(t => !t.IsDead)) return false; - - bool typeMatch = false; - - for (int i = 0; i < Targets.Count; i++) - { - // No specified cause of death required or missing cause of death - if (requiredCauseOfDeath == CauseOfDeathType.Unknown || Targets[i].CauseOfDeath == null) - { - typeMatch = true; - } - else - { - switch (Targets[i].CauseOfDeath.Type) - { - // If a cause of death is labeled as unknown, side with the traitor and accept this regardless of the required type - case CauseOfDeathType.Unknown: - typeMatch = true; - break; - case CauseOfDeathType.Pressure: - case CauseOfDeathType.Suffocation: - case CauseOfDeathType.Drowning: - typeMatch = requiredCauseOfDeath == Targets[i].CauseOfDeath.Type; - break; - case CauseOfDeathType.Affliction: - typeMatch = Targets[i].CauseOfDeath.Type == requiredCauseOfDeath && Targets[i].CauseOfDeath.Affliction.Identifier == afflictionId; - break; - case CauseOfDeathType.Disconnected: - typeMatch = false; - break; - } - } - - if (targetHull != null) - { - if (Targets[i].CurrentHull != null) - { - if (typeMatch && Targets[i].CurrentHull.RoomName == targetHull || Targets[i].CurrentHull.RoomName.Contains(targetHull)) - { - continue; - } - else - { - return false; - } - } - else - { - // Outside the submarine, not supported for now - return false; - } - } - else - { - if (typeMatch) - { - continue; - } - else - { - return false; - } - } - } - - return true; - } - - private LocalizedString GetCauseOfDeath() - { - if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty) - { - return requiredCauseOfDeath.ToString().ToLower(); - } - else - { - return TextManager.Get($"afflictionname.{afflictionId}").ToLower(); - } - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - - Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); - return Targets != null && !Targets.All(t => t.IsDead); - } - - public GoalKillTarget(TraitorMission.CharacterFilter filter, CauseOfDeathType requiredCauseOfDeath, string afflictionId, string targetHull, int targetCount, float targetPercentage) : base() - { - Filter = filter; - this.requiredCauseOfDeath = requiredCauseOfDeath; - this.afflictionId = afflictionId; - this.targetHull = targetHull; - this.targetCount = targetCount; - this.targetPercentage = targetPercentage / 100f; - - if (this.targetPercentage < 1f) - { - if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull == null) - { - InfoTextId = "traitorgoalkilltargetinfo"; - } - else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull == null) - { - InfoTextId = "traitorgoalkilltargetinfowithcause"; - } - else if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull != null) - { - InfoTextId = "traitorgoalkilltargetinfowithhull"; - } - else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull != null) - { - InfoTextId = "traitorgoalkilltargetinfowithcauseandhull"; - } - } - else - { - InfoTextId = "traitorgoalkilleveryoneinfo"; - } - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs deleted file mode 100644 index 8db03ab3c..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FarseerPhysics; -using Microsoft.Xna.Framework; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalReachDistanceFromSub : Goal - { - private readonly float requiredDistance; - private readonly float requiredDistanceSqr; - private float requiredDistanceInMeters; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistanceInMeters:0.00}" }); - - public override bool IsCompleted - { - get - { - return Traitors.Any(traitor => - { - Submarine ownSub = null; - - for (int i = 0; i < Submarine.MainSubs.Length; i++) - { - if (Submarine.MainSubs[i] != null && Submarine.MainSubs[i].TeamID == traitor.Character.TeamID) - { - ownSub = Submarine.MainSubs[i]; - break; - } - } - - if (ownSub == null) return false; - - var characterPosition = traitor.Character.WorldPosition; - var submarinePosition = ownSub.WorldPosition; - var distance = Vector2.DistanceSquared(characterPosition, submarinePosition); - return distance >= requiredDistanceSqr; - }); - } - } - - public GoalReachDistanceFromSub(float requiredDistance) : base() - { - InfoTextId = "TraitorGoalReachDistanceFromSub"; - requiredDistanceInMeters = requiredDistance; - this.requiredDistance = requiredDistance / Physics.DisplayToRealWorldRatio; - requiredDistanceSqr = this.requiredDistance * this.requiredDistance; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs deleted file mode 100644 index fe4edd2d4..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public class GoalReplaceInventory : HumanoidGoal - { - private readonly HashSet sabotageContainerIds = new HashSet(); - private readonly HashSet validReplacementIds = new HashSet(); - - private readonly float replaceAmount; - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override IEnumerable StatusTextKeys => base.StatusTextKeys.Concat(new string[] { "[percentage]" }); - public override IEnumerable StatusTextValues(Traitor traitor) => base.StatusTextValues(traitor).Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) }); - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - int totalAmount = 0, replacedAmount = 0; - foreach (var item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) - { - continue; - } - if (item.FindParentInventory(inventory => inventory.Owner is Character) != null) - { - continue; - } - if (sabotageContainerIds.Contains(((MapEntity)item).Prefab.Identifier)) - { - ++totalAmount; - if (item.OwnInventory.AllItems.All(containedItem => !validReplacementIds.Contains(containedItem.Prefab.Identifier))) - { - continue; - } - ++replacedAmount; - } - } - isCompleted = replacedAmount >= (int)(replaceAmount * totalAmount + 0.5f); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - if (sabotageContainerIds.Count <= 0 || validReplacementIds.Count <= 0) - { - return false; - } - return true; - } - - public GoalReplaceInventory(Identifier[] containerIds, Identifier[] replacementIds, float replaceAmount) - { - sabotageContainerIds.UnionWith(containerIds); - validReplacementIds.UnionWith(replacementIds); - this.replaceAmount = replaceAmount; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs deleted file mode 100644 index 0175429b9..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalSabotageItems : HumanoidGoal - { - private readonly string tag; - private readonly float conditionThreshold; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[tag]", "[target]", "[threshold]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private readonly List targetItems = new List(); - private string targetItemPrefabName = null; - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - foreach (var item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (item.Condition > conditionThreshold && (item.Prefab?.Identifier == tag || item.HasTag(tag))) - { - targetItems.Add(item); - } - } - if (targetItems.Count > 0) - { - var textId = targetItems[0].Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name.Value; - } - return targetItems.Count > 0; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = targetItems.All(item => item.Condition <= conditionThreshold); - } - - public GoalSabotageItems(string tag, float conditionThreshold) : base() - { - this.tag = tag; - this.conditionThreshold = conditionThreshold; - InfoTextId = "TraitorGoalSabotageInfo"; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs deleted file mode 100644 index 2f1314d49..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalUnwiring : HumanoidGoal - { - private readonly string tag; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[connectionname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetItemPrefabName ?? "", targetConnectionDisplayName ?? targetConnectionName }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private readonly List targetConnectionPanels = new List(); - private string targetItemPrefabName; - private string targetConnectionName; - private string targetConnectionDisplayName; - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - foreach (var item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (item.Prefab?.Identifier == tag || item.HasTag(tag)) - { - var connectionPanel = item.GetComponent(); - if (connectionPanel != null) - { - targetConnectionPanels.Add(connectionPanel); - } - } - } - if (targetConnectionPanels.Count > 0) - { - var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name.Value; - } - - return targetConnectionPanels.Count > 0; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = AreTargetsUnwired(); - } - - private bool AreTargetsUnwired() - { - for (int i = 0; i < targetConnectionPanels.Count; i++) - { - for (int j = 0; j < targetConnectionPanels[i].Connections.Count; j++) - { - if (targetConnectionPanels[i].Connections[j] == null || targetConnectionPanels[i].Connections[j].Wires == null) continue; - if (targetConnectionName != string.Empty) - { - if (targetConnectionPanels[i].Connections[j].Name != targetConnectionName) continue; - } - if (!targetConnectionPanels[i].Connections[j].Wires.All(w => w == null)) return false; - } - } - - return true; - } - - public GoalUnwiring(string tag, string targetConnectionName, string targetConnectionDisplayTag) : base() - { - this.tag = tag; - this.targetConnectionName = targetConnectionName; - - if (targetConnectionDisplayTag != string.Empty) - { - targetConnectionDisplayName = TextManager.FormatServerMessage(targetConnectionDisplayTag); - InfoTextId = "TraitorGoalUnwireInfo"; - } - else - { - InfoTextId = "TraitorGoalUnwireAllInfo"; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs deleted file mode 100644 index 02e344392..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalWaitForTraitors : Goal - { - private readonly int requiredCount; - private int count = 0; - - public override bool IsCompleted => count >= requiredCount; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[remaining]", "[count]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredCount - count}", $"{requiredCount}" }); - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - ++count; - return true; - } - - public GoalWaitForTraitors(int requiredCount) : base() - { - this.requiredCount = requiredCount; - InfoTextId = "TraitorGoalWaitForTraitorsInfoText"; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs deleted file mode 100644 index 06261de00..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Barotrauma -{ - partial class Traitor - { - public abstract class HumanoidGoal : Goal - { - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - return traitor?.Character?.IsHumanoid ?? false; - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs deleted file mode 100644 index f4b5d894d..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalHasDuration : Modifier - { - private readonly float requiredDuration; - private readonly bool countTotalDuration; - private readonly string durationInfoTextId; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" }); - - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString(CultureInfo.InvariantCulture) }); - - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - { - var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, - ("[infotext]", infoText), ("[duration]", requiredDuration.ToString(CultureInfo.InvariantCulture))) : infoText; - } - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private float remainingDuration = float.NaN; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - if (Goal.IsCompleted) - { - if (!float.IsNaN(remainingDuration)) - { - remainingDuration -= deltaTime; - } - else - { - remainingDuration = requiredDuration; - } - isCompleted |= remainingDuration <= 0.0f; - } - else if (!countTotalDuration) - { - remainingDuration = float.NaN; - } - } - - public GoalHasDuration(Goal goal, float requiredDuration, bool countTotalDuration, string durationInfoTextId) : base(goal) - { - this.requiredDuration = requiredDuration; - this.countTotalDuration = countTotalDuration; - this.durationInfoTextId = durationInfoTextId; - } - } - } -} - diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs deleted file mode 100644 index f2603e594..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalHasTimeLimit : Modifier - { - private readonly float timeLimit; - private readonly string timeLimitInfoTextId; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[timelimit]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" }); - - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - { - var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, ("[infotext]", infoText), ("[timelimit]", $"{TimeSpan.FromSeconds(timeLimit):g}")) : infoText; - } - - public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f); - - private float timeRemaining; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - timeRemaining = System.Math.Max(0.0f, timeRemaining - deltaTime); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - timeRemaining = timeLimit; - return true; - } - - public GoalHasTimeLimit(Goal goal, float timeLimit, string timeLimitInfoTextId) : base(goal) - { - this.timeLimit = timeLimit; - this.timeLimitInfoTextId = timeLimitInfoTextId; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs deleted file mode 100644 index 22b5a0a74..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalIsOptional : Modifier - { - private readonly string optionalInfoTextId; - - public override string StatusValueTextId => (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)) ? "failed" : base.StatusValueTextId; - - public override IEnumerable StatusTextValues(Traitor traitor) - { - var values = base.StatusTextValues(traitor).ToArray(); - values[1] = TextManager.FormatServerMessage(StatusValueTextId); - return values; - } - - public override bool IsCompleted => base.IsCompleted || (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)); - public override bool CanBeCompleted(ICollection traitors) => true; - - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - { - var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, ("[infotext]", infoText)) : infoText; - } - - public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal) - { - this.optionalInfoTextId = optionalInfoTextId; - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs deleted file mode 100644 index 2a48fe8ae..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public abstract class Modifier : Goal - { - protected Goal Goal { get; } - - public override string StatusValueTextId => Goal.StatusValueTextId; - - public override string StatusTextId - { - get => Goal.StatusTextId; - set => Goal.StatusTextId = value; - } - - public override string InfoTextId - { - get => Goal.InfoTextId; - set => Goal.InfoTextId = value; - } - - public override string CompletedTextId - { - get => Goal.CompletedTextId; - set => Goal.CompletedTextId = value; - } - - public override IEnumerable StatusTextKeys => Goal.StatusTextKeys; - public override IEnumerable StatusTextValues(Traitor traitor) => new string[] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; - - public override IEnumerable InfoTextKeys => Goal.InfoTextKeys; - public override IEnumerable InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor); - - public override IEnumerable CompletedTextKeys => Goal.CompletedTextKeys; - public override IEnumerable CompletedTextValues(Traitor traitor) => Goal.CompletedTextValues(traitor); - - protected internal override string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetStatusText(traitor, textId, keys, values); - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetInfoText(traitor, textId, keys, values); - protected internal override string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetCompletedText(traitor, textId, keys, values); - - public override string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor)); - public override string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor)); - public override string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor); - - public override bool IsCompleted => Goal.IsCompleted; - public override bool IsStarted(Traitor traitor) => base.IsStarted(traitor) && Goal.IsStarted(traitor); - public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && Goal.CanBeCompleted(traitors); - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || Goal.IsEnemy(character); - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - Goal.Update(deltaTime); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - if (!Goal.Start(traitor)) - { - return false; - } - return true; - } - - protected Modifier(Goal goal) : base() - { - Goal = goal; - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs deleted file mode 100644 index 8c09c73ef..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public class Objective - { - public Traitor Traitor { get; private set; } - - private int shuffleGoalsCount; - - private readonly List allGoals = new List(); - private readonly List activeGoals = new List(); - private readonly List pendingGoals = new List(); - private readonly List completedGoals = new List(); - - public bool IsCompleted => pendingGoals.Count <= 0; - public bool IsPartiallyCompleted => completedGoals.Count > 0; - public bool IsStarted { get; private set; } = false; - public bool CanBeStarted(ICollection traitors) => !IsStarted && allGoals.Any(goal => goal.CanBeCompleted(traitors)); - public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted(goal.Traitors)); - - public bool IsEnemy(Character character) => pendingGoals.Any(goal => goal.IsEnemy(character)); - public bool IsAllowedToDamage(Structure structure) => pendingGoals.Any(goal => goal.IsAllowedToDamage(structure)); - - public readonly HashSet Roles = new HashSet(); - - public string InfoText { get; private set; } - - public virtual string GoalInfoFormatId { get; set; } = "TraitorObjectiveGoalInfoFormat"; - - public string GoalInfos => - string.Join("/", - string.Join("/", activeGoals.Select((goal, index) => - { - var statusText = goal.StatusText(Traitor); - var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; - }).ToArray()), - string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); - - public string AllGoalInfos => - string.Join("/", - string.Join("/", allGoals.Select((goal, index) => - { - var statusText = goal.StatusText(Traitor); - var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; - }).ToArray()), - string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); - - public virtual string StartMessageTextId { get; set; } = "TraitorObjectiveStartMessage"; - public virtual IEnumerable StartMessageKeys => new string[] { "[traitorgoalinfos]" }; - public virtual IEnumerable StartMessageValues => new string[] { GoalInfos }; - - public virtual LocalizedString StartMessageText - => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageTextId, StartMessageKeys.Zip(StartMessageValues, (k,v) => (k,v)).ToArray()); - - public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer"; - public virtual IEnumerable StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" }); - public virtual IEnumerable StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" }); - - public virtual LocalizedString StartMessageServerText => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageServerTextId, StartMessageServerKeys.Zip(StartMessageServerValues, (k,v) => (k,v)).ToArray()); - - public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess"; - public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead"; - public virtual string EndMessageSuccessDetainedTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDetained"; - public virtual string EndMessageFailureTextId { get; set; } = "TraitorObjectiveEndMessageFailure"; - public virtual string EndMessageFailureDeadTextId { get; set; } = "TraitorObjectiveEndMessageFailureDead"; - public virtual string EndMessageFailureDetainedTextId { get; set; } = "TraitorObjectiveEndMessageFailureDetained"; - - public virtual IEnumerable EndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; - public virtual IEnumerable EndMessageValues => new string[] { Traitor?.Character?.Name ?? "(unknown)", GoalInfos }; - public virtual string EndMessageText - { - get - { - var traitorIsDead = Traitor.Character.IsDead; - var traitorIsDetained = Traitor.Character.LockHands; - var messageId = IsCompleted - ? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId) - : (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId); - return TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, messageId, EndMessageKeys.Zip(EndMessageValues, (k,v)=>(k,v)).ToArray()); - } - } - - public bool Start(Traitor traitor) - { - Traitor = traitor; - - activeGoals.Clear(); - pendingGoals.Clear(); - completedGoals.Clear(); - - var allGoalsCount = allGoals.Count; - var indices = allGoals.Select((goal, index) => index).ToArray(); - if (shuffleGoalsCount > 0) - { - for (var i = allGoalsCount; i > 1;) - { - int j = TraitorManager.RandomInt(i--); - var temp = indices[j]; - indices[j] = indices[i]; - indices[i] = temp; - } - } - - for (var i = 0; i < allGoalsCount; ++i) - { - var goal = allGoals[indices[i]]; - if (goal.Start(traitor)) - { - activeGoals.Add(goal); - pendingGoals.Add(goal); - if (shuffleGoalsCount > 0 && pendingGoals.Count >= shuffleGoalsCount) - { - break; - } - } - else - { - completedGoals.Add(goal); - } - } - - if (pendingGoals.Count <= 0 && completedGoals.Count < allGoals.Count) - { - return false; - } - - IsStarted = true; - - traitor.SendChatMessageBox(StartMessageText.Value, traitor.Mission.Identifier); - traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission.Identifier); - - return true; - } - - public void StartMessage() - { - Traitor.SendChatMessage(StartMessageText.Value, Traitor.Mission.Identifier); - } - - public void EndMessage() - { - Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission.Identifier); - Traitor.SendChatMessage(EndMessageText, Traitor.Mission.Identifier); - } - - public void Update(float deltaTime) - { - if (!IsStarted) - { - return; - } - for (int i = 0; i < pendingGoals.Count;) - { - var goal = pendingGoals[i]; - goal.Update(deltaTime); - if (!goal.IsCompleted) - { - ++i; - } - else - { - completedGoals.Add(goal); - pendingGoals.RemoveAt(i); - if (GameMain.Server != null) - { - Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission.Identifier); - if (pendingGoals.Count > 0) - { - Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission.Identifier); - } - Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission.Identifier); - } - } - } - } - - public Objective(string infoText, int shuffleGoalsCount, ICollection roles, ICollection goals) - { - InfoText = infoText; - this.shuffleGoalsCount = shuffleGoalsCount; - Roles.UnionWith(roles); - allGoals.AddRange(goals); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs deleted file mode 100644 index f8ce8c1d8..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class Traitor - { - public readonly Character Character; - - public string Role { get; } - public TraitorMission Mission { get; } - public Objective CurrentObjective => Mission.GetCurrentObjective(this); - - public Traitor(TraitorMission mission, string role, Character character) - { - Mission = mission; - Role = role; - Character = character; - Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData()); - } - - public delegate void MessageSender(string message); - - public void SendChatMessage(string serverText, Identifier iconIdentifier) - { - Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server); - } - - public void SendChatMessageBox(string serverText, Identifier iconIdentifier) - { - Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox); - } - - public void UpdateCurrentObjective(string objectiveText, Identifier iconIdentifier) - { - Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - Character.TraitorCurrentObjective = objectiveText; - GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective.Value, iconIdentifier, TraitorMessageType.Objective); - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index fe1c89f73..e17a1de58 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -1,207 +1,505 @@ -// #define DISABLE_MISSIONS +#nullable enable -using System; +using Barotrauma.Extensions; using Barotrauma.Networking; -using Lidgren.Network; -using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { - partial class TraitorManager - { - public static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks); + sealed partial class TraitorManager + { + const int MaxPreviousEventHistory = 10; - // All traitor related functionality should use the following interface for generating random values - public static int RandomInt(int n) => Random.Next(n); + const int StartDelayMin = 60; + const int StartDelayMax = 200; - // All traitor related functionality should use the following interface for generating random values - public static double RandomDouble() => Random.NextDouble(); + private float startTimer; + private bool started = false; - public readonly Dictionary Missions = new Dictionary(); + private TraitorResults? results = null; - public string GetCodeWords(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : ""; - public string GetCodeResponse(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : ""; - - public IEnumerable Traitors => Missions.Values.SelectMany(mission => mission.Traitors.Values); - - private float startCountdown = 0.0f; - private GameServer server; - - public bool ShouldEndRound + public class PreviousTraitorEvent { - get; - set; + public TraitorEventPrefab TraitorEvent { get; } + public TraitorEvent.State State { get; } + + public Client Traitor => + GameMain.Server.ConnectedClients.Find(c => traitorAccountId.IsSome() && traitorAccountId == c.AccountId) ?? + GameMain.Server.ConnectedClients.Find(c => traitorAddress == c.Connection.Endpoint.Address); + + private readonly Address traitorAddress; + private readonly Option traitorAccountId; + + public PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Client traitor) + { + TraitorEvent = traitorEvent; + State = state; + traitorAddress = traitor.Connection.Endpoint.Address; + traitorAccountId = traitor.AccountId; + } + + private PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Option accountId, Address address) + { + TraitorEvent = traitorEvent; + State = state; + traitorAddress = address; + traitorAccountId = accountId; + } + + public void Save(XElement parentElement) + { + parentElement.Add( + new XElement(nameof(PreviousTraitorEvent), + new XAttribute("id", TraitorEvent.Identifier), + new XAttribute("state", State), + new XAttribute("accountid", traitorAccountId), + new XAttribute("address", traitorAddress))); + } + + public static PreviousTraitorEvent? Load(XElement subElement) + { + var traitorEventId = subElement.GetAttributeIdentifier("id", Identifier.Empty); + var state = subElement.GetAttributeEnum("state", Barotrauma.TraitorEvent.State.Failed); + var accountId = Networking.AccountId.Parse( + subElement.GetAttributeString("accountid", null) + ?? subElement.GetAttributeString("steamid", "")); + var address = Address.Parse( + subElement.GetAttributeString("address", null) + ?? subElement.GetAttributeString("endpoint", "")) + .Fallback(new UnknownAddress()); + if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab? prefab) && prefab is TraitorEventPrefab traitorEventPrefab) + { + return new PreviousTraitorEvent(traitorEventPrefab, state, accountId, address); + } + else + { + DebugConsole.ThrowError($"Error when loading {nameof(TraitorManager)}: could not find a traitor event prefab with the identifier \"{traitorEventId}\"."); + return null; + } + } } + public readonly record struct ActiveTraitorEvent( + Client Traitor, + TraitorEvent TraitorEvent); + + private readonly List previousTraitorEvents = new List(); + + private readonly List activeEvents = new List(); + + public IEnumerable ActiveEvents => activeEvents; + + private readonly GameServer server; + + private EventManager? eventManager; + private Level? level; + + public bool Enabled; + public bool IsTraitor(Character character) { - if (Traitors == null) + return activeEvents.Any(e => e.Traitor.Character == character); + } + + public TraitorManager(GameServer server) + { + this.server = server; + } + + public void Initialize(EventManager eventManager, Level level) + { + this.eventManager = eventManager; + this.level = level; + startTimer = Rand.Range(StartDelayMin, StartDelayMax); + started = false; + results = null; + } + + private bool TryCreateTraitorEvents(EventManager eventManager, Level level) + { + var eventPrefabs = EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Cast(); + if (!eventPrefabs.Any()) { + DebugConsole.AddWarning("No traitor event available in any of the enabled content packages."); return false; } - return Traitors.Any(traitor => traitor.Character == character); + + if (server.ConnectedClients.Count(IsClientViableTraitor) < server.ServerSettings.TraitorsMinPlayerCount) + { + DebugConsole.AddWarning("Not enough clients to create a traitor event. Active traitor events: " + activeEvents.Count); +#if DEBUG + DebugConsole.AddWarning("Starting a traitor event anyway because this is a debug build."); +#else + return false; +#endif + + } + + int maxDangerLevel = server.ServerSettings.TraitorDangerLevel; + + int playerCount = server.ConnectedClients.Count(c => c.Character != null && !c.Character.Removed); + + var campaign = GameMain.GameSession?.Campaign; + var suitablePrefabs = eventPrefabs + .Where(e => EventManager.IsLevelSuitable(e, level)) + .Where(e => e.ReputationRequirementsMet(campaign)) + .Where(e => e.MissionRequirementsMet(GameMain.GameSession)) + .Where(e => e.LevelRequirementsMet(level)) + .Where(e => e.DangerLevel <= maxDangerLevel) + .Where(e => playerCount >= e.MinPlayerCount) + .ToList(); + + var minDangerLevelPrefabs = suitablePrefabs.FindAll(e => e.DangerLevel == TraitorEventPrefab.MinDangerLevel); + + if (!suitablePrefabs.Any()) + { + //this is normal, there e.g. might be no missions for an abandoned outpost or end level + DebugConsole.Log("No suitable traitor missions available for the level."); + return false; + } + + foreach (var previousEvent in previousTraitorEvents.DistinctBy(e => e.Traitor).Reverse()) + { + if (previousEvent.State == TraitorEvent.State.Completed && + previousEvent.TraitorEvent.DangerLevel < maxDangerLevel && + previousEvent.TraitorEvent.IsChainable && + IsClientViableTraitor(previousEvent.Traitor)) + { + GameServer.Log($"{NetworkMember.ClientLogName(previousEvent.Traitor)} successfully completed a traitor event on a previous round. Attempting to give choose them a new, more dangerous event...", ServerLog.MessageType.Traitors); + + var suitablePrefab = + //try finding an event that's continuation from the previous one (= requires the previous one to be completed 1st) + suitablePrefabs.GetRandomUnsynced(p => p.RequiredCompletedTags.Any(t => + previousEvent.TraitorEvent.Identifier == t || previousEvent.TraitorEvent.Tags.Contains(t))); + + suitablePrefab ??= + //otherwise try finding some higher-difficult event for the same faction + suitablePrefabs.GetRandomUnsynced(p => + p.RequiredCompletedTags.None() && + p.DangerLevel > previousEvent.TraitorEvent.DangerLevel && + p.Faction == previousEvent.TraitorEvent.Faction); + + if (suitablePrefab != null) + { + CreateTraitorEvent(eventManager, suitablePrefab, previousEvent.Traitor); + return true; + } + else + { + GameServer.Log($"Could not find a suitable, more difficult traitor event for {NetworkMember.ClientLogName(previousEvent.Traitor)}.", ServerLog.MessageType.Traitors); + } + } + } + + TraitorEventPrefab selectedPrefab; + if (GameMain.GameSession?.Campaign == null) + { + selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); + } + else + { + selectedPrefab = minDangerLevelPrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); + if (selectedPrefab == null) + { + GameServer.Log($"Could not find a suitable danger level {TraitorEventPrefab.MinDangerLevel} traitor event. Choosing a random event instead.", ServerLog.MessageType.Traitors); + selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); + } + } + + if (selectedPrefab != null) + { + var selectedTraitor = SelectRandomTraitor(); + if (selectedTraitor == null) + { + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\"."); + return false; + } + CreateTraitorEvent(eventManager, selectedPrefab, selectedTraitor); + return true; + } + + return false; } - public string GetTraitorRole(Character character) + private Client? SelectRandomTraitor() { - var traitor = Traitors.FirstOrDefault(candidate => candidate.Character == character); + if (GameSettings.CurrentConfig.VerboseLogging) + { + GameServer.Log( + $"Choosing a random traitor... Available traitors:" + + string.Concat(server.ConnectedClients.Where(IsClientViableTraitor).Select(c => $"\n - {c.Name} ({(int)(GetTraitorProbability(c) * 100)}%)")), + ServerLog.MessageType.Traitors); + } + return server.ConnectedClients.Where(IsClientViableTraitor).GetRandomByWeight(GetTraitorProbability, Rand.RandSync.Unsynced); + } + + private IEnumerable SelectSecondaryTraitors(TraitorEvent traitorEvent, Client mainTraitor) + { + if (traitorEvent.Prefab.SecondaryTraitorPercentage <= 0.0f && + traitorEvent.Prefab.SecondaryTraitorAmount <= 0) + { + return Enumerable.Empty(); + } + + var viableTraitors = server.ConnectedClients.Where(c => c != mainTraitor && IsClientViableTraitor(c)).ToList(); + + int amountToChoose = (int)Math.Ceiling(viableTraitors.Count * (traitorEvent.Prefab.SecondaryTraitorPercentage / 100.0f)); + amountToChoose = Math.Max(amountToChoose, traitorEvent.Prefab.SecondaryTraitorAmount); + + if (amountToChoose > viableTraitors.Count) + { + DebugConsole.ThrowError( + $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors."+ + $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors."); + amountToChoose = viableTraitors.Count; + } + + List traitors = new List(); + for (int i = 0; i < amountToChoose; i++) + { + var traitor = viableTraitors.GetRandomUnsynced(); + viableTraitors.Remove(traitor); + traitors.Add(traitor); + } + return traitors; + } + + private bool IsClientViableTraitor(Client client) + { + return + client != null && + server.ConnectedClients.Contains(client) && + client.Character != null && + !client.Character.IsIncapacitated && !client.Character.Removed && + activeEvents.None(e => e.Traitor == client); + } + + private float GetTraitorEventPrefabCommonness(TraitorEventPrefab prefab) + { + int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.TraitorEvent == prefab); + if (roundsSinceLastSelected.HasValue) + { + float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value); + //exponentially decreasing commonness the closer the last time the event was selected + return prefab.Commonness * normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected; + } + else + { + return prefab.Commonness; + } + } + + private float GetTraitorProbability(Client client) + { + int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.Traitor == client); + if (roundsSinceLastSelected.HasValue) + { + float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value); + //exponentially decreasing commonness the closer the last time the event was selected + return normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected; + } + else + { + return 1.0f; + } + } + + private int? GetRoundsSinceLastSelected(Func condition) + { + //most recent events are at the end of the list, start from there + for (int i = previousTraitorEvents.Count - 1; i >= 0; i--) + { + if (condition(previousTraitorEvents[i])) + { + return previousTraitorEvents.Count - i; + } + } + return null; + } + + private void CreateTraitorEvent(EventManager eventManager, TraitorEventPrefab selectedPrefab, Client traitor) + { + if (selectedPrefab.TryCreateInstance(out var newEvent)) + { + var secondaryTraitors = SelectSecondaryTraitors(newEvent, traitor); + + string logMessage = $"{NetworkMember.ClientLogName(traitor)} was selected as a traitor. Selected event: {selectedPrefab.Name}"; + if (secondaryTraitors.Any()) + { + logMessage += $", secondary traitors: {string.Join(", ", secondaryTraitors.Select(c => NetworkMember.ClientLogName(c)))}"; + } + GameServer.Log(logMessage, ServerLog.MessageType.Traitors); + + newEvent.OnStateChanged += () => SendCurrentState(newEvent); + activeEvents.Add(new ActiveTraitorEvent(traitor, newEvent)); + newEvent.SetTraitor(traitor); + newEvent.SetSecondaryTraitors(secondaryTraitors); + eventManager.ActivateEvent(newEvent); + SendCurrentState(newEvent); + } + else + { + DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!"); + } + } + + public void ForceTraitorEvent(TraitorEventPrefab traitorEventPrefab) + { + if (eventManager == null) + { + throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly."); + } + var traitor = SelectRandomTraitor(); if (traitor == null) { - return ""; + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\"."); + return; } - return traitor.Role; - } - - public TraitorManager() - { - } - - public void Start(GameServer server) - { -#if DISABLE_MISSIONS - return; -#endif - if (server == null) { return; } - - ShouldEndRound = false; - - this.server = server; - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)RandomDouble()); + CreateTraitorEvent(eventManager, traitorEventPrefab, traitor); } public void SkipStartDelay() { - startCountdown = 0.01f; + startTimer = 0.0f; + if (activeEvents.Any()) { started = true; } } public void Update(float deltaTime) { - if (ShouldEndRound) { return; } - -#if DISABLE_MISSIONS - return; -#endif - if (Missions.Any()) + if (!Enabled) { return; } + if (!started) { - bool missionCompleted = false; - bool gameShouldEnd = false; - CharacterTeamType winningTeam = CharacterTeamType.None; - foreach (var mission in Missions) + if (level?.LevelData is { Type: LevelData.LevelType.LocationConnection }) { - mission.Value.Update(deltaTime, () => + if (Submarine.MainSub.WorldPosition.X > level.Size.X / 2) { - switch (mission.Key) - { - case CharacterTeamType.Team1: - winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team2 : CharacterTeamType.None; - break; - case CharacterTeamType.Team2: - winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team1 : CharacterTeamType.None; - break; - default: - break; - } - gameShouldEnd = true; - }); - if (!gameShouldEnd && mission.Value.IsCompleted) - { - missionCompleted = true; - foreach (var traitor in mission.Value.Traitors.Values) - { - traitor.UpdateCurrentObjective("", mission.Value.Identifier); - } + //try starting ASAP if the submarine is already half-way through the level + //(brief delay regardless, because otherwise we might retry every frame if finding a suitable event fails below) + startTimer = Math.Min(startTimer, 10.0f); } } - if (gameShouldEnd) + startTimer -= deltaTime; + if (startTimer >= 0.0f) { return; } + if (eventManager == null) { - GameMain.GameSession.WinningTeam = winningTeam; - ShouldEndRound = true; - return; + throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly."); } - if (missionCompleted) + if (level == null) { - Missions.Clear(); - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); + throw new InvalidOperationException("Level was null. TraitorManager may not have been initialized properly."); } + if (TryCreateTraitorEvents(eventManager, level)) + { + started = true; + } + else + { + //restart timer, we might be able to start a mission later if more clients join + startTimer = Rand.Range(StartDelayMin, StartDelayMax); + } } - else if (startCountdown > 0.0f && server.GameStarted) + } + + public void EndRound() + { + Client? votedAsTraitor = GetClientAccusedAsTraitor(); + foreach (var activeEvent in activeEvents) { - startCountdown -= deltaTime; - if (startCountdown <= 0.0f) + if (results != null) { - int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0); - if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount) + DebugConsole.AddWarning("Multiple traitor events active during the round, only displaying the results for the last one."); + } + results = new TraitorResults(votedAsTraitor, activeEvent.TraitorEvent); + if (results.Value.MoneyPenalty > 0) + { + GameMain.GameSession?.Campaign?.Bank?.TryDeduct(results.Value.MoneyPenalty); + } + + if (activeEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed) + { + activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed; + } + GameServer.Log( + NetworkMember.ClientLogName(activeEvent.Traitor) + + (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed ? + " completed their traitor objective successfully." : " failed to complete their traitor objective."), + ServerLog.MessageType.Traitors); + + //consider the event failed if the traitor was identifier correctly, + //so the traitor doesn't get rewards or get assigned a follow-up event + if (results.Value.VotedCorrectTraitor) + { + GameServer.Log( + NetworkMember.ClientLogName(activeEvent.Traitor) + " was correctly identified as the traitor, and will not receive any rewards.", + ServerLog.MessageType.Traitors); + activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed; + } + previousTraitorEvents.Add(new PreviousTraitorEvent( + activeEvent.TraitorEvent.Prefab, + activeEvent.TraitorEvent.CurrentState, + activeEvent.Traitor)); + } + if (previousTraitorEvents.Count > MaxPreviousEventHistory) + { + previousTraitorEvents.RemoveRange(0, previousTraitorEvents.Count - MaxPreviousEventHistory); + } + activeEvents.Clear(); + } + + public Client? GetClientAccusedAsTraitor() + { + Client? votedAsTraitor = Voting.HighestVoted(VoteType.Traitor, server.ConnectedClients.Where(c => c.Character is { IsDead: false }), out int voteCount); + if (voteCount < server.ConnectedClients.Count * server.ServerSettings.MinPercentageOfPlayersForTraitorAccusation / 100.0f) + { + //at least x% of the players must've voted for the same player + votedAsTraitor = null; + } + return votedAsTraitor; + } + + public TraitorResults? GetEndResults() + { + return results; + } + + public XElement Save() + { + var element = new XElement(nameof(TraitorManager), + new XAttribute("version", GameMain.Version.ToString())); + foreach (var previousEvent in previousTraitorEvents) + { + previousEvent.Save(element); + } + return element; + } + + public void Load(XElement traitorManagerElement) + { + previousTraitorEvents.Clear(); + foreach (XElement subElement in traitorManagerElement.Elements()) + { + if (subElement.Name.ToIdentifier() == nameof(PreviousTraitorEvent)) + { + var previousTraitorEvent = PreviousTraitorEvent.Load(subElement); + if (previousTraitorEvent != null) { - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); - return; + previousTraitorEvents.Add(previousTraitorEvent); } - if (Character.CharacterList.Count(c => !c.IsDead && c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) <= 1) - { - return; - } - if (GameMain.GameSession.Missions.Any(m => m is CombatMission)) - { - var teamIds = new[] { CharacterTeamType.Team1, CharacterTeamType.Team2 }; - foreach (var teamId in teamIds) - { - if (server.ConnectedClients.Count(c => c.Character != null && !c.Character.IsDead && c.TeamID == teamId) < 2) - { - continue; - } - var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); - if (mission != null) - { - Missions.Add(teamId, mission); - } - } - var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key) ? 1 : 0); - if (canBeStartedCount >= Missions.Count) - { - var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key) ? 1 : 0); - if (startSuccessCount >= Missions.Count) - { - return; - } - } - } - else - { - var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); - if (mission != null) - { - if (mission.CanBeStarted(server, this, CharacterTeamType.None)) - { - if (mission.Start(server, this, CharacterTeamType.None)) - { - Missions.Add(CharacterTeamType.None, mission); - return; - } - } - } - } - Missions.Clear(); - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); } } } - public List GetEndResults() + public void SendCurrentState(TraitorEvent ev) { - List results = new List(); - -#if DISABLE_MISSIONS - return results; -#endif - if (GameMain.Server == null || !Missions.Any()) { return results; } - - foreach (var mission in Missions) - { - results.Add(new TraitorMissionResult(mission.Value)); - } - - return results; + if (ev?.Traitor == null) { return; } + var msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE); + msg.WriteByte((byte)ev.CurrentState); + msg.WriteIdentifier(ev.Prefab.Identifier); + server.SendTraitorMessage(msg, ev.Traitor); } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs deleted file mode 100644 index cbaeabea5..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ /dev/null @@ -1,394 +0,0 @@ -//#define ALLOW_SOLO_TRAITOR -//#define ALLOW_NONHUMANOID_TRAITOR - -using System; -using Barotrauma.Networking; -using Lidgren.Network; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Security.Cryptography; -using Barotrauma.Extensions; - -namespace Barotrauma -{ - partial class Traitor - { - public class TraitorMission - { - private static string wordsTxt = Path.Combine("Content", "CodeWords.txt"); - - private readonly List allObjectives = new List(); - private readonly List pendingObjectives = new List(); - private readonly List completedObjectives = new List(); - - /// - /// Has the mission been completed (does not mean that the traitor necessarily won, the mission is considered completed if the traitor fails for whatever reason) - /// - public bool IsCompleted => pendingObjectives.Count <= 0; - - public readonly Dictionary Traitors = new Dictionary(); - - public delegate bool RoleFilter(Character character); - public readonly Dictionary Roles = new Dictionary(); - - public string StartText { get; private set; } - public string CodeWords { get; private set; } - public string CodeResponse { get; private set; } - - public string GlobalEndMessageSuccessTextId { get; private set; } - public string GlobalEndMessageSuccessDeadTextId { get; private set; } - public string GlobalEndMessageSuccessDetainedTextId { get; private set; } - public string GlobalEndMessageFailureTextId { get; private set; } - public string GlobalEndMessageFailureDeadTextId { get; private set; } - public string GlobalEndMessageFailureDetainedTextId { get; private set; } - - public readonly Identifier Identifier; - - public virtual IEnumerable GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; - public virtual IEnumerable GlobalEndMessageValues { - get { - var isSuccess = completedObjectives.Count >= allObjectives.Count; - return new string[] { - string.Join(", ", Traitors.Values.Select(traitor => traitor.Character?.Name ?? "(unknown)")), - (isSuccess ? completedObjectives.LastOrDefault() : pendingObjectives.FirstOrDefault())?.GoalInfos ?? "" - }; - } - } - - public string GlobalEndMessage - { - get - { - if (Traitors.Any() && allObjectives.Count > 0) - { - return TextManager.JoinServerMessages("\n", - Traitors.Values.Select(traitor => - { - var isSuccess = completedObjectives.Count >= allObjectives.Count; - var traitorIsDead = traitor.Character.IsDead; - var traitorIsDetained = traitor.Character.LockHands; - var messageId = isSuccess - ? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId) - : (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId); - return TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, messageId, GlobalEndMessageKeys.Zip(GlobalEndMessageValues).ToArray()); - }).ToArray()); - } - return ""; - } - } - - public Objective GetCurrentObjective(Traitor traitor) - { - if (!Traitors.ContainsValue(traitor) || pendingObjectives.Count <= 0) - { - return null; - } - return pendingObjectives.Find(objective => objective.Roles.Contains(traitor.Role)); - } - - protected List> FindTraitorCandidates(GameServer server, CharacterTeamType team, RoleFilter traitorRoleFilter) - { - var traitorCandidates = new List>(); - foreach (Client c in server.ConnectedClients) - { - if (c.Character == null || c.Character.IsDead || c.Character.Removed || !traitorRoleFilter(c.Character) || - (team != CharacterTeamType.None && c.Character.TeamID != team)) - { - continue; - } -#if !ALLOW_NONHUMANOID_TRAITOR - if (!c.Character.IsHumanoid) { continue; } -#endif - traitorCandidates.Add(Tuple.Create(c, c.Character)); - } - return traitorCandidates; - } - - protected List FindCharacters() - { - List characters = new List(); - foreach (var character in Character.CharacterList) - { - characters.Add(character); - } - return characters; - } - - protected List>> AssignTraitors(GameServer server, TraitorManager traitorManager, CharacterTeamType team) - { - List characters = FindCharacters(); -#if !ALLOW_SOLO_TRAITOR - if (characters.Count < 2) - { - return null; - } -#endif - var roleCandidates = new Dictionary>>(); - foreach (var role in Roles) - { - roleCandidates.Add(role.Key, new HashSet>(FindTraitorCandidates(server, team, role.Value))); - if (roleCandidates[role.Key].Count <= 0) - { - return null; - } - } - var candidateRoleCounts = new Dictionary, int>(); - foreach (var candidateEntry in roleCandidates) - { - foreach (var candidate in candidateEntry.Value) - { - candidateRoleCounts[candidate] = candidateRoleCounts.TryGetValue(candidate, out var count) ? count + 1 : 1; - } - } - var unassignedRoles = new List(roleCandidates.Keys); - unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count); - var assignedCandidates = new List>>(); - while (unassignedRoles.Count > 0) - { - var currentRole = unassignedRoles[0]; - var availableCandidates = roleCandidates[currentRole].ToList(); - if (availableCandidates.Count <= 0) - { - break; - } - unassignedRoles.RemoveAt(0); - availableCandidates.Sort((a, b) => candidateRoleCounts[b] - candidateRoleCounts[a]); - unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count); - - int numCandidates = 1; - for (int i = 1; i < availableCandidates.Count && candidateRoleCounts[availableCandidates[i]] == candidateRoleCounts[availableCandidates[0]]; ++i) - { - ++numCandidates; - } - - var selected = ToolBox.SelectWeightedRandom(availableCandidates, availableCandidates.Select(c => Math.Max(c.Item1.RoundsSincePlayedAsTraitor, 0.1f)).ToList(), TraitorManager.Random); - assignedCandidates.Add(Tuple.Create(currentRole, selected)); - foreach (var candidate in roleCandidates.Values) - { - candidate.Remove(selected); - } - } - if (unassignedRoles.Count > 0) - { - return null; - } - return assignedCandidates; - } - - public bool CanBeStarted(GameServer server, TraitorManager traitorManager, CharacterTeamType team) - { - foreach (var role in Roles) - { - var candidates = FindTraitorCandidates(server, team, role.Value); - if (candidates.Count <= 0) - { - return false; - } - } - return AssignTraitors(server, traitorManager, team) != null; - } - - public bool Start(GameServer server, TraitorManager traitorManager, CharacterTeamType team) - { - var assignedCandidates = AssignTraitors(server, traitorManager, team); - if (assignedCandidates == null) - { - return false; - } - - foreach (Client client in server.ConnectedClients) - { - client.RoundsSincePlayedAsTraitor++; - } - - Traitors.Clear(); - foreach (var candidate in assignedCandidates) - { - var traitor = new Traitor(this, candidate.Item1, candidate.Item2.Item1.Character); - Traitors.Add(candidate.Item1, traitor); - candidate.Item2.Item1.RoundsSincePlayedAsTraitor = 0; - } - CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - - if (pendingObjectives.Count <= 0 || !pendingObjectives[0].CanBeStarted(Traitors.Values)) - { - Traitors.Clear(); - return false; - } - - var pendingMessages = new Dictionary>(); - pendingMessages.Clear(); - foreach (var traitor in Traitors.Values) - { - pendingMessages.Add(traitor, new List()); - } - pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); - pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); - - Update(0.0f, () => { GameMain.Server.TraitorManager.ShouldEndRound = true; }); -#if SERVER - foreach (var traitor in Traitors.Values) - { - GameServer.Log($"{GameServer.CharacterLogName(traitor.Character)} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); - } -#endif - return true; - } - - public delegate void TraitorWinHandler(); - - public void Update(float deltaTime, TraitorWinHandler winHandler) - { - if (pendingObjectives.Count <= 0 || Traitors.Count <= 0) - { - return; - } - if (Traitors.Values.Any(traitor => traitor.Character == null || traitor.Character.IsDead || traitor.Character.Removed)) - { - Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier)); - pendingObjectives.Clear(); - Traitors.Clear(); - return; - } - var startedObjectives = new List(); - foreach (var traitor in Traitors.Values) - { - startedObjectives.Clear(); - while (pendingObjectives.Count > 0) - { - var objective = GetCurrentObjective(traitor); - if (objective == null) - { - // No more objectives left for traitor or waiting for another traitor's objective. - break; - } - if (!objective.IsStarted) - { - if (!objective.Start(traitor)) - { - //the mission fails if an objective cannot be started - if (completedObjectives.Count > 0) - { - objective.EndMessage(); - } - pendingObjectives.Clear(); - break; - } - startedObjectives.Add(objective); - } - objective.Update(deltaTime); - if (objective.IsCompleted) - { - pendingObjectives.Remove(objective); - completedObjectives.Add(objective); - objective.EndMessage(); - continue; - } - if (objective.IsStarted && !objective.CanBeCompleted) - { - objective.EndMessage(); - pendingObjectives.Clear(); - } - break; - } - if (pendingObjectives.Count > 0) - { - startedObjectives.ForEach(objective => objective.StartMessage()); - } - } - if (completedObjectives.Count >= allObjectives.Count) - { - foreach (var traitor in Traitors) - { - SteamAchievementManager.OnTraitorWin(traitor.Value.Character); - } - winHandler(); - } - } - - public delegate bool CharacterFilter(Character character); - public List FindKillTarget(Character traitor, CharacterFilter filter, int count = -1, float percentage = -1f) - { - if (traitor == null) { return null; } - - List validCharacters = Character.CharacterList.FindAll(c => c.TeamID == traitor.TeamID && - c != traitor && !c.IsDead && - (filter == null || filter(c))); - - int targetCount = 1; - if (count > 0) - { - targetCount = count; - } - else if (percentage > 0f) - { - targetCount = (int)Math.Max(1, Math.Floor(validCharacters.Count * percentage)); - } - - List targetCharacters = new List(); - - if (validCharacters.Count > 0) - { - for (int i = 0; i < targetCount; i++) - { - if (validCharacters.Count == 0) break; - Character character = validCharacters[TraitorManager.RandomInt(validCharacters.Count)]; - targetCharacters.Add(character); - validCharacters.Remove(character); - } - return targetCharacters; - } - -#if ALLOW_SOLO_TRAITOR - targetCharacters.Add(traitor); - return targetCharacters; -#else - return null; -#endif - } - - public string GetTargetNames(List targets) - { - string names = string.Empty; - for (int i = 0; i < targets.Count; i++) - { - names += targets[i].Name; - - if (i < targets.Count - 1) - { - names += ", "; - } - } - - if (names.Length > 0) - { - return names; - } - else - { - return TextManager.FormatServerMessage("unknown"); - } - } - - public TraitorMission(Identifier identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) - { - Identifier = identifier; - StartText = startText; - GlobalEndMessageSuccessTextId = globalEndMessageSuccessTextId; - GlobalEndMessageSuccessDeadTextId = globalEndMessageSuccessDeadTextId; - GlobalEndMessageSuccessDetainedTextId = globalEndMessageSuccessDetainedTextId; - GlobalEndMessageFailureTextId = globalEndMessageFailureTextId; - GlobalEndMessageFailureDeadTextId = globalEndMessageFailureDeadTextId; - GlobalEndMessageFailureDetainedTextId = globalEndMessageFailureDetainedTextId; - foreach (var role in roles) - { - Roles.Add(role.Key, role.Value); - } - allObjectives.AddRange(objectives); - pendingObjectives.AddRange(objectives); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs deleted file mode 100644 index a9aa33810..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs +++ /dev/null @@ -1,664 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Xml.Linq; -using System.Linq; -using Barotrauma.Networking; - -namespace Barotrauma -{ - class TraitorMissionPrefab - { - public class TraitorMissionEntry : Prefab - { - public static PrefabCollection Prefabs => TraitorMissionPrefab.Prefabs; - - public readonly TraitorMissionPrefab Prefab; - public float SelectedWeight; - - public TraitorMissionEntry(ContentXElement element, TraitorMissionsFile file) : base(file, element) - { - Prefab = new TraitorMissionPrefab(element); - } - - public override void Dispose() { } - } - public static readonly PrefabCollection Prefabs = new PrefabCollection(); - - public static TraitorMissionPrefab RandomPrefab() - { - var selected = ToolBox.SelectWeightedRandom(Prefabs.ToList(), Prefabs.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); - //the weight of the missions that didn't get selected keeps growing the make them more likely to get picked - foreach (var mission in Prefabs) - { - mission.SelectedWeight += 10; - } - selected.SelectedWeight = 0.0f; - return selected.Prefab; - } - - private class AttributeChecker : IDisposable - { - private readonly XElement element; - private readonly HashSet required = new HashSet(); - private readonly HashSet optional = new HashSet(); - - public void Optional(params string[] names) - { - optional.UnionWith(names); - } - - public void Required(params string[] names) - { - required.UnionWith(names); - } - - public void Dispose() - { - foreach (var requiredName in required) - { - if (element.Attributes().All(attribute => attribute.Name != requiredName)) - { - GameServer.Log($"Required attribute \"{requiredName}\" is missing in \"{element.Name}\"", ServerLog.MessageType.Error); - } - } - foreach (var attribute in element.Attributes()) - { - var attributeName = attribute.Name.ToString(); - if (!required.Contains(attributeName) && !optional.Contains(attributeName)) - { - GameServer.Log($"Unsupported attribute \"{attributeName}\" in \"{element.Name}\"", ServerLog.MessageType.Error); - } - } - } - - public AttributeChecker(XElement element) - { - this.element = element; - } - } - - public class Goal - { - public readonly string Type; - public readonly XElement Config; - - public Goal(string type, XElement config) - { - Type = type; - Config = config; - } - - private delegate bool TargetFilter(string value, Character character); - private static Dictionary targetFilters = new Dictionary() - { - { "job", (value, character) => value == character.Info.Job.Prefab.Identifier }, - { "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) } - }; - - public Traitor.Goal Instantiate() - { - Traitor.Goal goal = null; - using (var checker = new AttributeChecker(Config)) - { - checker.Required("type"); - var goalType = Config.GetAttributeString("type", ""); - switch (goalType.ToLowerInvariant()) - { - case "killtarget": - { - checker.Optional(targetFilters.Keys.ToArray()); - checker.Optional("causeofdeath"); - checker.Optional("affliction"); - checker.Optional("roomname"); - checker.Optional("targetcount"); - checker.Optional("targetpercentage"); - List killFilters = new List(); - foreach (var attribute in Config.Attributes()) - { - if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) - { - killFilters.Add((character) => filter(attribute.Value, character)); - } - } - goal = new Traitor.GoalKillTarget((character) => killFilters.All(f => f(character)), - (CauseOfDeathType)Enum.Parse(typeof(CauseOfDeathType), Config.GetAttributeString("causeofdeath", "Unknown"), true), - Config.GetAttributeString("affliction", null), Config.GetAttributeString("targethull", null), Config.GetAttributeInt("targetcount", -1), - Config.GetAttributeFloat("targetpercentage", -1f)); - break; - } - case "destroyitems": - { - checker.Required("tag"); - checker.Optional("percentage", "matchIdentifier", "matchTag", "matchInventory"); - var tag = Config.GetAttributeString("tag", null); - if (tag != null) - { - goal = new Traitor.GoalDestroyItemsWithTag( - tag, - Config.GetAttributeFloat("percentage", 100.0f) / 100.0f, - Config.GetAttributeBool("matchIdentifier", true), - Config.GetAttributeBool("matchTag", true), - Config.GetAttributeBool("matchInventory", false)); - } - break; - } - case "sabotage": - { - checker.Required("tag"); - checker.Optional("threshold"); - var tag = Config.GetAttributeString("tag", null); - if (tag != null) - { - goal = new Traitor.GoalSabotageItems(tag, Config.GetAttributeFloat("threshold", 20.0f)); - } - break; - } - case "floodsub": - checker.Optional("percentage"); - goal = new Traitor.GoalFloodPercentOfSub(Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); - break; - case "finditem": - checker.Required("identifier"); - checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers", "percentage"); - List itemCountFilters = new List(); - foreach (var attribute in Config.Attributes()) - { - if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) - { - itemCountFilters.Add((character) => filter(attribute.Value, character)); - } - } - goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), - Config.GetAttributeString("identifier", - null), - Config.GetAttributeBool("preferNew", - true), - Config.GetAttributeBool("allowNew", - true), - Config.GetAttributeBool("allowExisting", - true), - Config.GetAttributeFloat("percentage", - -1f), - Config.GetAttributeIdentifierArray("allowedContainers", - new string[] - { - "steelcabinet", - "mediumsteelcabinet", - "suppliescabinet" - }.ToIdentifiers())); - break; - case "replaceinventory": - checker.Required("containers", "replacements"); - checker.Optional("percentage"); - goal = new Traitor.GoalReplaceInventory(Config.GetAttributeIdentifierArray("containers", new Identifier[] { }), Config.GetAttributeIdentifierArray("replacements", new Identifier[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); - break; - case "reachdistancefromsub": - checker.Optional("distance"); - goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 125f)); - break; - case "injectpoison": - checker.Optional(targetFilters.Keys.ToArray()); - checker.Required("poison"); - checker.Required("affliction"); - checker.Optional("targetcount"); - checker.Optional("targetpercentage"); - List poisonFilters = new List(); - foreach (var attribute in Config.Attributes()) - { - if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) - { - poisonFilters.Add((character) => filter(attribute.Value, character)); - } - } - goal = new Traitor.GoalInjectTarget((character) => poisonFilters.All(f => f(character)), Config.GetAttributeString("poison", null), - Config.GetAttributeString("affliction", null), Config.GetAttributeInt("targetcount", -1), Config.GetAttributeFloat("targetpercentage", -1f)); - break; - case "unwire": - checker.Required("tag"); - checker.Optional("connectionname"); - checker.Optional("connectiondisplayname"); - goal = new Traitor.GoalUnwiring(Config.GetAttributeString("tag", null), Config.GetAttributeString("connectionname", null), Config.GetAttributeString("connectiondisplayname)", null)); - break; - case "transformentity": - checker.Required("entities", "entitytypes"); - checker.Optional("catalystid"); - goal = new Traitor.GoalEntityTransformation(Config.GetAttributeStringArray("entities", null), Config.GetAttributeStringArray("entitytypes", null), Config.GetAttributeString("catalystid", null)); - break; - case "keeptransformedalive": - checker.Required("speciesname"); - goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeIdentifier("speciesname", Identifier.Empty)); - break; - default: - GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error); - break; - } - } - if (goal == null) - { - return null; - } - foreach (var element in Config.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "modifier": - { - using (var checker = new AttributeChecker(element)) - { - checker.Required("type"); - var modifierType = element.GetAttributeString("type", ""); - switch (modifierType) - { - case "duration": - { - checker.Optional("cumulative", "duration", "infotext"); - var isCumulative = element.GetAttributeBool("cumulative", false); - goal = new Traitor.GoalHasDuration(goal, element.GetAttributeFloat("duration", 5.0f), isCumulative, element.GetAttributeString("infotext", isCumulative ? "TraitorGoalWithCumulativeDurationInfoText" : "TraitorGoalWithDurationInfoText")); - break; - } - case "timelimit": - checker.Optional("timelimit", "infotext"); - goal = new Traitor.GoalHasTimeLimit(goal, element.GetAttributeFloat("timelimit", 180.0f), element.GetAttributeString("infotext", "TraitorGoalWithTimeLimitInfoText")); - break; - case "optional": - checker.Optional("infotext"); - goal = new Traitor.GoalIsOptional(goal, element.GetAttributeString("infotext", "TraitorGoalIsOptionalInfoText")); - break; - default: - GameServer.Log($"Unrecognized modifier type \"{modifierType}\".", ServerLog.MessageType.Error); - break; - } - } - break; - } - } - } - foreach (var element in Config.Elements()) - { - var elementName = element.Name.ToString().ToLowerInvariant(); - switch (elementName) - { - case "modifier": - // loaded above - break; - case "infotext": - { - using (var checker = new AttributeChecker(element)) - { - checker.Required("id"); - var id = element.GetAttributeString("id", null); - if (id != null) - { - goal.InfoTextId = id; - } - } - break; - } - case "completedtext": - { - using (var checker = new AttributeChecker(element)) - { - checker.Required("id"); - var id = element.GetAttributeString("id", null); - if (id != null) - { - goal.CompletedTextId = id; - } - } - break; - } - default: - GameServer.Log($"Unrecognized element \"{element.Name}\" in goal.", ServerLog.MessageType.Error); - break; - } - } - return goal; - } - } - - - public abstract class ObjectiveBase - { - public HashSet Roles { get; } = new HashSet(); - - public abstract void InstantiateGoals(); - public abstract Traitor.Objective Instantiate(IEnumerable roles); - } - - protected class Objective : ObjectiveBase - { - public string InfoText { get; internal set; } - public string StartMessageTextId { get; internal set; } - public string StartMessageServerTextId { get; internal set; } - public string EndMessageSuccessTextId { get; internal set; } - public string EndMessageSuccessDeadTextId { get; internal set; } - public string EndMessageSuccessDetainedTextId { get; internal set; } - public string EndMessageFailureTextId { get; internal set; } - public string EndMessageFailureDeadTextId { get; internal set; } - public string EndMessageFailureDetainedTextId { get; internal set; } - public int ShuffleGoalsCount { get; internal set; } - - public readonly List Goals = new List(); - - private List goalInstances = null; - - public override void InstantiateGoals() - { - goalInstances = Goals.ConvertAll(goal => - { - var instance = goal.Instantiate(); - if (instance == null) - { - GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error); - } - return instance; - }).FindAll(goal => goal != null); - } - - public override Traitor.Objective Instantiate(IEnumerable roles) - { - var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, roles.ToArray(), goalInstances); - if (StartMessageTextId != null) - { - result.StartMessageTextId = StartMessageTextId; - } - if (StartMessageServerTextId != null) - { - result.StartMessageServerTextId = StartMessageServerTextId; - } - if (EndMessageSuccessTextId != null) - { - result.EndMessageSuccessTextId = EndMessageSuccessTextId; - } - if (EndMessageSuccessDeadTextId != null) - { - result.EndMessageSuccessDeadTextId = EndMessageSuccessDeadTextId; - } - if (EndMessageSuccessDetainedTextId != null) - { - result.EndMessageSuccessDetainedTextId = EndMessageSuccessDetainedTextId; - } - if (EndMessageFailureTextId != null) - { - result.EndMessageFailureTextId = EndMessageFailureTextId; - } - if (EndMessageFailureDeadTextId != null) - { - result.EndMessageFailureDeadTextId = EndMessageFailureDeadTextId; - } - if (EndMessageFailureDetainedTextId != null) - { - result.EndMessageFailureDetainedTextId = EndMessageFailureDetainedTextId; - } - return result; - } - } - - protected class WaitObjective : ObjectiveBase - { - private Traitor.GoalWaitForTraitors sharedGoal; - - public override void InstantiateGoals() - { - sharedGoal = new Traitor.GoalWaitForTraitors(Roles.Count); - } - - public override Traitor.Objective Instantiate(IEnumerable roles) - { - return new Traitor.Objective("TraitorObjectiveInfoTextWaitForOtherTraitors", -1, roles.ToArray(), new[] { sharedGoal }); - } - - public WaitObjective(ICollection roles) - { - Roles.UnionWith(roles); - } - } - - public class Role - { - public readonly Traitor.TraitorMission.RoleFilter Filter; - - public Role(IEnumerable filters) - { - Filter = character => filters.All(filter => filter(character)); - } - - public Role() - { - Filter = character => true; - } - } - public readonly Dictionary Roles = new Dictionary(); - - public readonly Identifier Identifier; - public readonly string StartText; - public readonly string EndMessageSuccessText; - public readonly string EndMessageSuccessDeadText; - public readonly string EndMessageSuccessDetainedText; - public readonly string EndMessageFailureText; - public readonly string EndMessageFailureDeadText; - public readonly string EndMessageFailureDetainedText; - - public readonly List Objectives = new List(); - - public Traitor.TraitorMission Instantiate() - { - var objectivesWithSync = new List(); - var objectivesCount = Objectives.Count; - if (objectivesCount > 0) - { - var pendingRoles = new HashSet(); - var pendingCount = 1; - objectivesWithSync.Add(Objectives[0]); - pendingRoles.UnionWith(Objectives[0].Roles); - for (var i = 1; i < objectivesCount; ++i) - { - var objective = Objectives[i]; - if (pendingRoles.IsSupersetOf(objective.Roles)) - { - if (pendingCount > 1) - { - objectivesWithSync.Add(new WaitObjective(objective.Roles)); - } - pendingRoles.Clear(); - pendingCount = 0; - } - objectivesWithSync.Add(objective); - pendingRoles.UnionWith(objective.Roles); - ++pendingCount; - } - if (pendingCount > 1 && pendingRoles.IsSubsetOf(Roles.Keys)) - { - // TODO: If last objective includes only one traitor, other traitors will get the wrong end message. - objectivesWithSync.Add(new WaitObjective(Roles.Keys)); - } - } - - return new Traitor.TraitorMission( - Identifier, - StartText ?? "TraitorMissionStartMessage", - EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess", - EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead", - EndMessageSuccessDetainedText ?? "TraitorObjectiveEndMessageSuccessDetained", - EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure", - EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead", - EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained", - Roles.ToDictionary(kv => kv.Key, kv => kv.Value.Filter), - objectivesWithSync.SelectMany(objective => - { - objective.InstantiateGoals(); - return objective.Roles.Select(role => objective.Instantiate(new[] { role })); - }).ToArray()); - } - - protected Goal LoadGoal(XElement goalRoot) - { - var goalType = goalRoot.GetAttributeString("type", ""); - return new Goal(goalType, goalRoot); - } - - protected Objective LoadObjective(XElement objectiveRoot, string[] allRoles) - { - var allRolesSet = new HashSet(allRoles); - var result = new Objective - { - ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1) - }; - var objectiveRoles = objectiveRoot.GetAttributeStringArray("roles", allRoles); - if (!allRolesSet.IsSupersetOf(objectiveRoles)) - { - var unrecognized = new HashSet(objectiveRoles); - unrecognized.ExceptWith(allRoles); - GameServer.Log($"Undefined role(s) \"{string.Join(", ", unrecognized)}\" set for Objective.", ServerLog.MessageType.Error); - } - result.Roles.UnionWith(allRolesSet.Intersect(objectiveRoles)); - - foreach (var element in objectiveRoot.Elements()) - { - using (var checker = new AttributeChecker(element)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "infotext": - checker.Required("id"); - result.InfoText = element.GetAttributeString("id", null); - break; - case "startmessage": - checker.Required("id"); - result.StartMessageTextId = element.GetAttributeString("id", null); - break; - case "startmessageserver": - checker.Required("id"); - result.StartMessageServerTextId = element.GetAttributeString("id", null); - break; - case "endmessagesuccess": - checker.Required("id"); - result.EndMessageSuccessTextId = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdead": - checker.Required("id"); - result.EndMessageSuccessDeadTextId = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdetained": - checker.Required("id"); - result.EndMessageSuccessDetainedTextId = element.GetAttributeString("id", null); - break; - case "endmessagefailure": - checker.Required("id"); - result.EndMessageFailureTextId = element.GetAttributeString("id", null); - break; - case "endmessagefailuredead": - checker.Required("id"); - result.EndMessageFailureDeadTextId = element.GetAttributeString("id", null); - break; - case "endmessagefailuredetained": - checker.Required("id"); - result.EndMessageFailureDetainedTextId = element.GetAttributeString("id", null); - break; - case "goal": - { - var goal = LoadGoal(element); - if (goal != null) - { - result.Goals.Add(goal); - } - break; - } - default: - GameServer.Log($"Unrecognized element \"{element.Name}\" under Objective.", ServerLog.MessageType.Error); - break; - } - } - } - return result; - } - - protected Role LoadRole(XElement roleRoot) - { - var filters = new List(); - var jobs = roleRoot.GetAttributeStringArray("jobs", null); - if (jobs != null) - { - var jobsSet = new HashSet(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture))); - filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower().Value)); - } - return new Role(filters); - } - - public TraitorMissionPrefab(ContentXElement missionRoot) - { - Identifier = missionRoot.GetAttributeIdentifier("identifier", Identifier.Empty); - foreach (var element in missionRoot.Elements()) - { - using (var checker = new AttributeChecker(element)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "role": - checker.Required("id"); - checker.Optional("jobs"); - Roles.Add(element.GetAttributeString("id", null), LoadRole(element)); - break; - } - } - } - if (!Roles.Any()) - { - Roles.Add("traitor", new Role()); - } - foreach (var element in missionRoot.Elements()) - { - using (var checker = new AttributeChecker(element)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "role": - // handled above - break; - case "startinfotext": - checker.Required("id"); - StartText = element.GetAttributeString("id", null); - break; - case "endmessagesuccess": - checker.Required("id"); - EndMessageSuccessText = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdead": - checker.Required("id"); - EndMessageSuccessDeadText = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdetained": - checker.Required("id"); - EndMessageSuccessDetainedText = element.GetAttributeString("id", null); - break; - case "endmessagefailure": - checker.Required("id"); - EndMessageFailureText = element.GetAttributeString("id", null); - break; - case "endmessagefailuredead": - checker.Required("id"); - EndMessageFailureDeadText = element.GetAttributeString("id", null); - break; - case "endmessagefailuredetained": - checker.Required("id"); - EndMessageFailureDetainedText = element.GetAttributeString("id", null); - break; - case "objective": - { - var objective = LoadObjective(element, Roles.Keys.ToArray()); - if (objective != null) - { - Objectives.Add(objective); - } - break; - } - default: - GameServer.Log($"Unrecognized element \"{element.Name}\"under TraitorMission.", ServerLog.MessageType.Error); - break; - } - } - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs deleted file mode 100644 index 6fe640c28..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class TraitorMissionResult - { - public TraitorMissionResult(Traitor.TraitorMission mission) - { - MissionIdentifier = mission.Identifier; - EndMessage = mission.GlobalEndMessage; - Success = mission.IsCompleted; - foreach (Traitor traitor in mission.Traitors.Values) - { - Characters.Add(traitor.Character); - } - } - - public void ServerWrite(IWriteMessage msg) - { - msg.WriteIdentifier(MissionIdentifier); - msg.WriteString(EndMessage); - msg.WriteBoolean(Success); - msg.WriteByte((byte)Characters.Count); - foreach (Character character in Characters) - { - msg.WriteUInt16(character.ID); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 4c66ef099..898f1765a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -62,6 +62,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index a5069b8f3..2fc63b541 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -331,6 +331,11 @@ namespace Barotrauma } } if (targetSlot < 0) { return false; } + //the item should always stay in the Any slot if it's containable in one + if (pickable.AllowedSlots.Contains(InvSlotType.Any)) + { + targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot); + } return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character); } else @@ -521,8 +526,5 @@ namespace Barotrauma protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } - - public virtual void ClientRead(IReadMessage msg) { } - public virtual void ServerWrite(IWriteMessage msg) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index caa1b2919..9495c3e08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -344,12 +344,12 @@ namespace Barotrauma private Identifier GetTargetingTag(AITarget aiTarget) { if (aiTarget?.Entity == null) { return Identifier.Empty; } - string targetingTag = string.Empty; + Identifier targetingTag = Identifier.Empty; if (aiTarget.Entity is Character targetCharacter) { if (targetCharacter.IsDead) { - targetingTag = "dead"; + targetingTag = "dead".ToIdentifier(); } else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter)) { @@ -357,11 +357,11 @@ namespace Barotrauma } else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) { - targetingTag = "owner"; + targetingTag = "owner".ToIdentifier(); } else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter))) { - targetingTag = "hostile"; + targetingTag = "hostile".ToIdentifier(); } else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) { @@ -373,25 +373,25 @@ namespace Barotrauma { // Pets see other pets as pets by default. // Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below. - targetingTag = "pet"; + targetingTag = "pet".ToIdentifier(); } else if (targetCharacter.IsHusk && AIParams.HasTag("husk")) { - targetingTag = "husk"; + targetingTag = "husk".ToIdentifier(); } else if (!Character.IsSameSpeciesOrGroup(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { - targetingTag = "stronger"; + targetingTag = "stronger".ToIdentifier(); } else if (enemy.CombatStrength < CombatStrength) { - targetingTag = "weaker"; + targetingTag = "weaker".ToIdentifier(); } else { - targetingTag = "equal"; + targetingTag = "equal".ToIdentifier(); } } } @@ -406,27 +406,27 @@ namespace Barotrauma break; } } - if (targetingTag.IsNullOrEmpty()) + if (targetingTag.IsEmpty) { if (targetItem.GetComponent() != null) { - targetingTag = "sonar"; + targetingTag = "sonar".ToIdentifier(); } if (targetItem.GetComponent() != null) { - targetingTag = "door"; + targetingTag = "door".ToIdentifier(); } } } else if (aiTarget.Entity is Structure) { - targetingTag = "wall"; + targetingTag = "wall".ToIdentifier(); } else if (aiTarget.Entity is Hull) { - targetingTag = "room"; + targetingTag = "room".ToIdentifier(); } - return targetingTag.ToIdentifier(); + return targetingTag; } public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -767,7 +767,7 @@ namespace Barotrauma mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.TorsoTorque); } } - if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag("guardianshelter")) + if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag(Tags.GuardianShelter)) { if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine)) { @@ -864,10 +864,11 @@ namespace Barotrauma } // Ensure that the creature keeps inside the level SteerInsideLevel(deltaTime); - float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); - steeringManager.Update(speed); - float targetMovement = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed; - Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement); + float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); + //calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits + steeringManager.Update(1.0f); + float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed; + Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed); if (Character.CurrentHull != null && Character.AnimController.InWater) { // Limit the swimming speed inside the sub. @@ -1935,14 +1936,7 @@ namespace Barotrauma } else if (advance) { - if (pathSteering != null) - { - pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); - } - else - { - SteeringManager.SteeringSeek(steerPos, 10); - } + SteeringManager.SteeringSeek(steerPos, 10); } else { @@ -2310,7 +2304,7 @@ namespace Barotrauma if (damageTarget != null) { Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, Character); + item.Use(deltaTime, user: Character); } } } @@ -2545,6 +2539,7 @@ namespace Barotrauma item.body.LinearVelocity *= 0.9f; item.body.LinearVelocity -= velocity * 0.25f; bool wasBroken = item.Condition <= 0.0f; + item.LastEatenTime = (float)Timing.TotalTimeUnpaused; item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (item.Condition <= 0.0f) @@ -3116,6 +3111,13 @@ namespace Barotrauma // ignore if owner is tagged to be explicitly ignored (Feign Death) continue; } + var characterTargetingTag = GetTargetingTag(owner.AiTarget); + if (!characterTargetingTag.IsEmpty) + { + // if the enemy is configured to ignore the target character, ignore the provocative item they're holding/wearing too + var characterTargetingParams = GetTargetParams(characterTargetingTag); + if (characterTargetingParams?.State == AIState.Idle) { continue; } + } } } if (targetCharacter != null) @@ -4055,18 +4057,6 @@ namespace Barotrauma } return null; } - - public override void ServerWrite(IWriteMessage msg) - { - msg.WriteByte((byte)State); - PetBehavior?.ServerWrite(msg); - } - - public override void ClientRead(IReadMessage msg) - { - State = (AIState)msg.ReadByte(); - PetBehavior?.ClientRead(msg); - } } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 890772d58..43378e91a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -355,7 +355,7 @@ namespace Barotrauma Vector2 forward = VectorExtensions.Forward(rotation); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward)); if (angle > 70) { continue; } - if (!Character.CanSeeCharacter(c)) { continue; } + if (!Character.CanSeeTarget(c)) { continue; } if (dist < closestDistance || closestEnemy == null) { closestEnemy = c; @@ -465,7 +465,7 @@ namespace Barotrauma else { // Allows bots to heal targets autonomously while swimming outside of the sub. - if (AIObjectiveRescueAll.IsValidTarget(Character, Character)) + if (AIObjectiveRescueAll.IsValidTarget(Character, Character, out _)) { AddTargets(Character, Character); } @@ -480,24 +480,33 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); - if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) + var currentObjective = objectiveManager.CurrentObjective; + bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); + if (currentObjective is AIObjectiveGoTo goTo) { - if (Character.CurrentHull == null) + if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam) { - run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300; + // NPCs with a wait order don't run. + run = false; } - else + else if (goTo.Target != null) { - float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y; - if (Math.Abs(yDiff) > 100) + if (Character.CurrentHull == null) { - run = true; + run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300; } else { - float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X; - run = Math.Abs(xDiff) > 500; + float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y; + if (Math.Abs(yDiff) > 100) + { + run = true; + } + else + { + float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X; + run = Math.Abs(xDiff) > 500; + } } } } @@ -577,7 +586,7 @@ namespace Barotrauma if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } if (Character.CurrentHull == null) { return; } - bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1); + bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1); bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) @@ -594,7 +603,7 @@ namespace Barotrauma if (findItemState != FindItemState.OtherItem) { var decontain = ObjectiveManager.GetActiveObjectives().LastOrDefault(); - if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) && + if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) { // Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker. @@ -680,8 +689,8 @@ namespace Barotrauma } if (removeDivingSuit) { - var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); - if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS)) + var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear); + if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors)) { if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { @@ -723,9 +732,9 @@ namespace Barotrauma } if (takeMaskOff) { - if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR)) + if (Character.HasEquippedItem(Tags.LightDivingGear)) { - var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR); + 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 })) @@ -928,7 +937,7 @@ namespace Barotrauma if (isPreferencesDefined) { // Use any valid locker as a fall back container. - return container.Item.HasTag("locker") ? 0.5f : 0; + return container.Item.HasTag(Tags.FallbackLocker) ? 0.5f : 0; } return 1; } @@ -1069,7 +1078,7 @@ namespace Barotrauma foreach (Character target in Character.CharacterList) { if (target.CurrentHull != hull) { continue; } - if (AIObjectiveRescueAll.IsValidTarget(target, Character)) + if (AIObjectiveRescueAll.IsValidTarget(target, Character, out _)) { if (AddTargets(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective()) { @@ -1287,6 +1296,7 @@ namespace Barotrauma minorDamageThreshold = 10; majorDamageThreshold = 40; } + bool eitherIsMentallyUnstable = IsMentallyUnstable || attacker.AIController is { IsMentallyUnstable: true }; if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) @@ -1296,7 +1306,7 @@ namespace Barotrauma return; } float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); - bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; + bool isAccidental = attacker.IsBot && !eitherIsMentallyUnstable && attacker.CombatAction == null; if (isAccidental) { if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)) @@ -1306,7 +1316,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { @@ -1378,6 +1388,11 @@ namespace Barotrauma if (otherCharacter.IsPlayer) { continue; } if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; } + if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker)) + { + // 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); if (!isWitnessing) { @@ -1411,6 +1426,10 @@ namespace Barotrauma humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() is Controller ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; } + if (c.IsKiller) + { + return AIObjectiveCombat.CombatMode.Offensive; + } return humanAI.ObjectiveManager.IsCurrentOrder() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ? @@ -1442,6 +1461,10 @@ namespace Barotrauma isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } + if (c.IsKiller) + { + return AIObjectiveCombat.CombatMode.Offensive; + } if (isWitnessing && c.CombatAction != null && !c.IsSecurity) { return c.CombatAction.WitnessReaction; @@ -1452,7 +1475,7 @@ namespace Barotrauma isAttackerFightingEnemy = true; return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } - if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) + if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable) { if (c.IsSecurity) { @@ -1653,7 +1676,7 @@ namespace Barotrauma needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure; return needsAir || needsSuit; } - if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) + if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { return needsAir; } @@ -1666,14 +1689,14 @@ namespace Barotrauma /// Check whether the character has a diving suit in usable condition plus some oxygen. /// public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) - => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true, + => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true, predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes)); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) - => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true); + => HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true); private static List matchingItems = new List(); @@ -1739,7 +1762,7 @@ namespace Barotrauma { continue; } - if (!otherCharacter.CanSeeCharacter(character)) { continue; } + if (!otherCharacter.CanSeeTarget(character)) { continue; } if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; @@ -1816,7 +1839,7 @@ namespace Barotrauma bool someoneSpoke = false; bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false; - if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) + if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem)) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1827,14 +1850,14 @@ namespace Barotrauma continue; } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } - if (!otherCharacter.CanSeeCharacter(thief)) { continue; } + if (!otherCharacter.CanSeeTarget(thief)) { 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) { var connectedHulls = thief.Submarine.GetHulls(alsoFromConnectedSubs: true); - if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; } - if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } + if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; } + if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } } if (!someoneSpoke) { @@ -1966,7 +1989,7 @@ namespace Barotrauma foreach (var c in Character.CharacterList) { if (c.CurrentHull != hull) { continue; } - if (AIObjectiveRescueAll.IsValidTarget(c, character)) + if (AIObjectiveRescueAll.IsValidTarget(c, character, out _)) { AddTargets(character, c); } @@ -2171,7 +2194,10 @@ namespace Barotrauma { bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); - if (!teamGood) { return false; } + if (!teamGood) + { + return other.IsHusk && me.IsDisguisedAsHusk; + } if (other.IsPet) { // Hostile NPCs are hostile to all pets, unless they are in the same team. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5f26687da..db44b0290 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -241,7 +241,6 @@ namespace Barotrauma float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority); findPathTimer = priority * Rand.Range(1.0f, 1.2f); IsPathDirty = false; - return DiffToCurrentNode(); void SkipCurrentPathNodes() { @@ -284,15 +283,6 @@ namespace Barotrauma } Vector2 diff = DiffToCurrentNode(); - var collider = character.AnimController.Collider; - // Only humanoids can climb ladders - bool canClimb = character.AnimController is HumanoidAnimController; - //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically - if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius) - { - // TODO: might cause some edge cases -> do we need this? - diff.Y = 0.0f; - } if (diff == Vector2.Zero) { return Vector2.Zero; } return Vector2.Normalize(diff) * weight; } @@ -401,16 +391,15 @@ namespace Barotrauma else { bool nextLadderSameAsCurrent = currentLadder == nextLadder; + float colliderHeight = collider.Height / 2 + collider.Radius; + float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; + float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); if (currentLadder != null && nextLadder != null) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } - //at the same height as the waypoint - float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderHeight = collider.Height / 2 + collider.Radius; - float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); - if (heightDiff < colliderHeight * 1.25f) + if (Math.Abs(heightDiff) < colliderHeight * 1.25f) { if (nextLadder != null && !nextLadderSameAsCurrent) { @@ -463,10 +452,10 @@ namespace Barotrauma } } } - return ConvertUnits.ToSimUnits(diff); } else if (character.AnimController.InWater) { + // Swimming var door = currentPath.CurrentNode.ConnectedDoor; if (door == null || door.CanBeTraversed) { @@ -502,6 +491,13 @@ namespace Barotrauma bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y; var door = currentPath.CurrentNode.ConnectedDoor; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); + float colliderHeight = collider.Height / 2 + collider.Radius; + float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; + if (heightDiff < colliderHeight) + { + //the waypoint is between the top and bottom of the collider, no need to move vertically. + diff.Y = 0.0f; + } if (currentPath.CurrentNode.Stairs != null) { bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 2fb0e0027..46e1e6aff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -328,7 +328,7 @@ namespace Barotrauma if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; } //check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method) - if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; } + if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeTarget(s))) { return false; } //check if the character has an appropriate personality if (selectedConversation.allowedSpeakerTags.Count > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index dd3b79d0c..ee3bd03c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -40,6 +40,7 @@ namespace Barotrauma public virtual bool AllowOutsideSubmarine => false; public virtual bool AllowInFriendlySubs => false; public virtual bool AllowInAnySub => false; + public virtual bool AllowWhileHandcuffed => true; protected readonly List subObjectives = new List(); private float _cumulatedDevotion; @@ -246,32 +247,43 @@ namespace Barotrauma { get { - if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) - { - if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) - { - return false; - } - } + if (!AllowWhileHandcuffed && character.LockHands) { return false; } if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } + // Evaluate ignored at outpost first, because it has higher priority than AllowInAnySub or AllowInFriendlySubs. + if (IsIgnoredAtOutpost()) { return false; } if (AllowInAnySub) { return true; } if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; } - return character.Submarine.TeamID == character.TeamID || - character.Submarine.TeamID == character.OriginalTeamID || - character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID); + return character.Submarine.TeamID == character.TeamID || character.Submarine.TeamID == character.OriginalTeamID; } } + /// + /// Returns true only when at a friendly outpost and when the order is set to be ignored there. + /// Note that even if this returns false, the objective can be disallowed, because AllowInFriendlySubs is false. + /// + public bool IsIgnoredAtOutpost() + { + if (!IgnoreAtOutpost) { return false; } + if (!Level.IsLoadedFriendlyOutpost) { return false; } + if (!character.IsOnPlayerTeam) { return false; } + if (character.Submarine?.Info == null) { return false; } + return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC; + } + + protected void HandleNonAllowed() + { + Priority = 0; + Abandon = !IsIgnoredAtOutpost(); + } + protected virtual float GetPriority() { - bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } - if (isOrder) + if (objectiveManager.IsOrder(this)) { Priority = objectiveManager.GetOrderPriority(this); } @@ -446,7 +458,7 @@ namespace Barotrauma } } - protected virtual bool Check() + private bool Check() { if (AbortCondition != null && AbortCondition(this)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index e3e8af4cd..e43bb2622 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -19,6 +19,7 @@ namespace Barotrauma protected override bool Filter(PowerContainer battery) { if (battery == null) { return false; } + if (battery.OutputDisabled) { return false; } var item = battery.Item; if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index d9c0c85ec..45d99f33a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; + public override bool AllowWhileHandcuffed => false; public readonly Item item; public bool IsPriority { get; set; } @@ -30,8 +31,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } else @@ -119,18 +119,21 @@ namespace Barotrauma if (item.IgnoreByAI(character)) { Abandon = true; - return false; } - if (item.ParentInventory != null) + else if (item.ParentInventory != null) { - if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) + if (!objectiveManager.HasOrder()) + { + // Don't allow taking items from containers in the idle state. + Abandon = true; + } + else if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character)) { // Target was picked up or moved by someone. Abandon = true; - return false; } } - return IsCompleted; + return !Abandon && IsCompleted; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 7d82e8377..72277a406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -14,8 +14,6 @@ namespace Barotrauma public readonly List prioritizedItems = new List(); - public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier(); - protected override int MaxTargets => 100; public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1) @@ -83,9 +81,8 @@ namespace Barotrauma return true; } - public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => - allowUnloading && - container.HasTag(AllowCleanupTag) && + public static bool IsValidContainer(Item container, Character character) => + container.HasTag(Tags.AllowCleanup) && container.HasAccess(character) && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && container.GetComponent() != null && @@ -103,15 +100,18 @@ namespace Barotrauma // In a character inventory return false; } - if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } + if (!allowUnloading) { return false; } + if (!IsValidContainer(item.Container, character)) { return false; } } if (!item.HasAccess(character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } + //something (e.g. a pet) was eating the item within the last second - don't clean up + if (item.LastEatenTime > Timing.TotalTimeUnpaused - 1.0) { return false; } var wire = item.GetComponent(); if (wire != null) { - if (wire.Connections.Any()) { return false; } + if (wire.Connections.Any(c => c != null)) { return false; } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index a1b25992a..612eaa7fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -46,7 +46,6 @@ namespace Barotrauma _weapon = value; _weaponComponent = null; hasAimed = false; - RemoveSubObjective(ref seekAmmunitionObjective); } } private ItemComponent _weaponComponent; @@ -55,14 +54,7 @@ namespace Barotrauma get { if (Weapon == null) { return null; } - if (_weaponComponent == null) - { - _weaponComponent = - Weapon.GetComponent() ?? - Weapon.GetComponent() ?? - Weapon.GetComponent() as ItemComponent; - } - return _weaponComponent; + return _weaponComponent ?? GetWeaponComponent(Weapon); } } @@ -280,13 +272,13 @@ namespace Barotrauma } break; case CombatMode.Arrest: - if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true)) + if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true)) { IsCompleted = true; } else if (Enemy.IsKnockedDown && !objectiveManager.IsCurrentObjective() && - !HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false)) + !HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false)) { IsCompleted = true; } @@ -348,8 +340,10 @@ namespace Barotrauma if (character.LockHands || Enemy == null) { Weapon = null; + RemoveSubObjective(ref seekAmmunitionObjective); return false; } + bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; if (checkWeaponsTimer < 0) { checkWeaponsTimer = checkWeaponsInterval; @@ -375,7 +369,7 @@ namespace Barotrauma // All good, the weapon is loaded break; } - if (Reload(seekAmmo: false)) + if (Reload(seekAmmo: isAllowedToSeekWeapons)) { // All good, we can use the weapon. break; @@ -407,7 +401,6 @@ namespace Barotrauma } } } - bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; if (!isAllowedToSeekWeapons) { if (WeaponComponent == null) @@ -416,7 +409,7 @@ namespace Barotrauma Mode = CombatMode.Retreat; } } - else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority)) + else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority))) { // Poor weapon equipped -> try to find better. RemoveSubObjective(ref seekAmmunitionObjective); @@ -431,27 +424,10 @@ namespace Barotrauma { if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; } if (i.IsOwnedBy(character)) { return 0; } - var mw = i.GetComponent(); - var rw = i.GetComponent(); float priority = 0; - if (mw != null) + if (GetWeaponComponent(i) is ItemComponent ic) { - priority = mw.CombatPriority / 100; - } - else if (rw != null) - { - priority = rw.CombatPriority / 100; - } - if (i.HasTag("stunner")) - { - if (Mode == CombatMode.Arrest) - { - priority *= 2; - } - else - { - priority /= 2; - } + priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100; } return priority; } @@ -477,6 +453,7 @@ namespace Barotrauma if (!CheckWeapon(seekAmmo: false)) { Weapon = null; + RemoveSubObjective(ref seekAmmunitionObjective); } } return Weapon != null; @@ -521,111 +498,164 @@ namespace Barotrauma private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent); + private static ItemComponent GetWeaponComponent(Item item) => + item.GetComponent() ?? + item.GetComponent() ?? + item.GetComponent() ?? + item.GetComponent() as ItemComponent; + + private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg) + { + lethalDmg = -1; + float priority = weapon.CombatPriority; + if (weapon is RepairTool repairTool) + { + switch (repairTool.UsableIn) + { + case RepairTool.UseEnvironment.Air: + if (character.InWater) { return 0; } + break; + case RepairTool.UseEnvironment.Water: + if (!character.InWater) { return 0; } + break; + case RepairTool.UseEnvironment.None: + return 0; + case RepairTool.UseEnvironment.Both: + default: + break; + } + } + if (prioritizeMelee && weapon is MeleeWeapon) + { + priority *= 5; + } + if (weapon.IsEmpty(character)) + { + if (weapon is RangedWeapon && isCloseToEnemy) + { + // Ignore weapons that don't have any ammunition (-> Don't seek ammo). + return 0; + } + else + { + // Reduce the priority for weapons that don't have proper ammunition loaded. + if (character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType)) + { + // Yet prefer the equipped weapon. + priority *= 0.75f; + } + else + { + priority *= 0.5f; + } + } + } + if (Enemy.Params.Health.StunImmunity) + { + if (weapon.Item.HasTag(Tags.StunnerItem)) + { + priority /= 2; + } + } + else if (Enemy.IsKnockedDown) + { + // Enemy is stunned, reduce the priority of stunner weapons. + Attack attack = GetAttackDefinition(weapon); + if (attack != null) + { + lethalDmg = attack.GetTotalDamage(); + float max = lethalDmg + 1; + if (weapon.Item.HasTag(Tags.StunnerItem)) + { + priority = max; + } + else + { + float stunDmg = ApproximateStunDamage(weapon, attack); + float diff = stunDmg - lethalDmg; + priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max); + } + } + } + else if (Mode == CombatMode.Arrest) + { + // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons. + if (weapon.Item.HasTag(Tags.StunnerItem)) + { + priority *= 5; + } + else + { + Attack attack = GetAttackDefinition(weapon); + if (attack != null) + { + lethalDmg = attack.GetTotalDamage(); + float stunDmg = ApproximateStunDamage(weapon, attack); + float diff = stunDmg - lethalDmg; + if (diff < 0) + { + priority /= 2; + } + } + } + } + else if (weapon is MeleeWeapon && weapon.Item.HasTag(Tags.StunnerItem) && (Enemy.Params.Health.StunImmunity || !CanMeleeStunnerStun(weapon))) + { + // Cannot do stun damage -> use the melee damage to determine the priority. + Attack attack = GetAttackDefinition(weapon); + priority = attack?.GetTotalDamage() ?? priority / 2; + } + return priority; + } + + private float ApproximateStunDamage(ItemComponent weapon, Attack attack) + { + // Try to reduce the priority using the actual damage values and status effects. + // This is an approximation, because we can't check the status effect conditions here. + // The result might be incorrect if there is a high stun effect that's only applied in certain conditions. + var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character)); + if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List hitEffects)) + { + statusEffects = statusEffects.Concat(hitEffects); + } + float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); + float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => + { + float stunAmount = 0; + var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); + if (stunAffliction != null) + { + stunAmount = stunAffliction.Strength; + } + return stunAmount; + }); + return attack.Stun + afflictionsStun + effectsStun; + } + + private bool CanMeleeStunnerStun(ItemComponent weapon) + { + // If there's an item container that takes a battery, + // assume that it's required for the stun effect + // as we can't check the status effect conditions here. + var mobileBatteryTag = Tags.MobileBattery; + var containers = weapon.Item.Components.Where(ic => + ic is ItemContainer container && + container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); + // If there's no such container, assume that the melee weapon can stun without a battery. + return containers.None() || containers.Any(container => + (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); + } + private Item GetWeapon(IEnumerable weaponList, out ItemComponent weaponComponent) { weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; - bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300); + bool isCloseToEnemy = IsEnemyCloserThan(300); bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character); foreach (var weapon in weaponList) { - float priority = weapon.CombatPriority; - if (weapon is RepairTool repairTool) - { - switch (repairTool.UsableIn) - { - case RepairTool.UseEnvironment.Air: - if (character.InWater) { continue; } - break; - case RepairTool.UseEnvironment.Water: - if (!character.InWater) { continue; } - break; - case RepairTool.UseEnvironment.None: - continue; - case RepairTool.UseEnvironment.Both: - default: - break; - } - } - if (prioritizeMelee) - { - if (weapon is MeleeWeapon) - { - priority *= 5; - } - else - { - priority /= 2; - } - } - if (weapon.IsEmpty(character)) - { - if (weapon is RangedWeapon && !isAllowedToSeekWeapons) - { - // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo). - continue; - } - else - { - // Halve the priority for weapons that don't have proper ammunition loaded. - priority /= 2; - } - } - if (Enemy.Params.Health.StunImmunity) - { - if (weapon.Item.HasTag("stunner")) - { - priority /= 2; - } - } - else if (Enemy.IsKnockedDown) - { - // Enemy is stunned, reduce the priority of stunner weapons. - Attack attack = GetAttackDefinition(weapon); - if (attack != null) - { - lethalDmg = attack.GetTotalDamage(); - float max = lethalDmg + 1; - if (weapon.Item.HasTag("stunner")) - { - priority = max; - } - else - { - float stunDmg = ApproximateStunDamage(weapon, attack); - float diff = stunDmg - lethalDmg; - priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max); - } - } - } - else if (Mode == CombatMode.Arrest) - { - // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons. - if (weapon.Item.HasTag("stunner")) - { - priority *= 2; - } - else - { - Attack attack = GetAttackDefinition(weapon); - if (attack != null) - { - lethalDmg = attack.GetTotalDamage(); - float stunDmg = ApproximateStunDamage(weapon, attack); - float diff = stunDmg - lethalDmg; - if (diff < 0) - { - priority /= 2; - } - } - } - } - else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon)) - { - Attack attack = GetAttackDefinition(weapon); - priority = attack?.GetTotalDamage() ?? priority / 2; - } + float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg); if (priority > bestPriority) { weaponComponent = weapon; @@ -636,7 +666,7 @@ namespace Barotrauma if (bestPriority < 1) { return null; } if (Mode == CombatMode.Arrest) { - if (weaponComponent.Item.HasTag("stunner")) + if (weaponComponent.Item.HasTag(Tags.StunnerItem)) { isLethalWeapon = false; } @@ -654,44 +684,6 @@ namespace Barotrauma } } return weaponComponent.Item; - - float ApproximateStunDamage(ItemComponent weapon, Attack attack) - { - // Try to reduce the priority using the actual damage values and status effects. - // This is an approximation, because we can't check the status effect conditions here. - // The result might be incorrect if there is a high stun effect that's only applied in certain conditions. - var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character)); - if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List hitEffects)) - { - statusEffects = statusEffects.Concat(hitEffects); - } - float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); - float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => - { - float stunAmount = 0; - var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); - if (stunAffliction != null) - { - stunAmount = stunAffliction.Strength; - } - return stunAmount; - }); - return attack.Stun + afflictionsStun + effectsStun; - } - - bool CanMeleeStunnerStun(ItemComponent weapon) - { - // If there's an item container that takes a battery, - // assume that it's required for the stun effect - // as we can't check the status effect conditions here. - var mobileBatteryTag = "mobilebattery".ToIdentifier(); - var containers = weapon.Item.Components.Where(ic => - ic is ItemContainer container && - container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); - // If there's no such container, assume that the melee weapon can stun without a battery. - return containers.None() || containers.Any(container => - (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); - } } public static float GetLethalDamage(ItemComponent weapon) @@ -771,13 +763,13 @@ namespace Barotrauma { return false; } - if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType)) + if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType)) { //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter character.ClearInput(InputType.Aim); character.ClearInput(InputType.Shoot); Weapon.TryInteract(character, forceSelectKey: true); - var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); + var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); @@ -791,8 +783,6 @@ namespace Barotrauma } } return true; - - static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); } private float findHullTimer; @@ -926,7 +916,7 @@ namespace Barotrauma if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) { - if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _)) + if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _)) { if (!arrestingRegistered) { @@ -986,7 +976,7 @@ namespace Barotrauma foreach (var item in Enemy.Inventory.AllItemsMod) { if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound || - item.HasTag("weapon") || + item.HasTag(Tags.Weapon) || item.GetComponent() != null || item.GetComponent() != null) { @@ -997,9 +987,9 @@ namespace Barotrauma } //prefer using handcuffs already on the enemy's inventory - if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable matchingItems)) + if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable matchingItems)) { - HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems); + HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems); } if (matchingItems.Any() && @@ -1079,7 +1069,7 @@ namespace Barotrauma if (ammunitionIdentifiers != null) { // Try reload ammunition from inventory - static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); + 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); if (ammunition != null) { @@ -1205,7 +1195,7 @@ namespace Barotrauma myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) - var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); + var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); foreach (var body in pickedBodies) { Character target = null; @@ -1248,7 +1238,7 @@ namespace Barotrauma } } character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); + Weapon.Use(deltaTime, user: character); reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed); } @@ -1265,7 +1255,7 @@ namespace Barotrauma { Unequip(); } - SteeringManager.Reset(); + SteeringManager?.Reset(); } protected override void OnAbandon() @@ -1275,7 +1265,7 @@ namespace Barotrauma { Unequip(); } - SteeringManager.Reset(); + SteeringManager?.Reset(); } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 244b66a89..53db54d77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -11,6 +11,7 @@ namespace Barotrauma class AIObjectiveContainItem: AIObjective { public override Identifier Identifier { get; set; } = "contain item".ToIdentifier(); + public override bool AllowWhileHandcuffed => false; public Func GetItemPriority; @@ -109,7 +110,7 @@ namespace Barotrauma private bool CheckItem(Item item) { - return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); + return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character) && container.ShouldBeContained(item, out _); } protected override void Act(float deltaTime) @@ -226,7 +227,10 @@ namespace Barotrauma AllowToFindDivingGear = AllowToFindDivingGear, AllowDangerousPressure = AllowDangerousPressure, TargetCondition = ConditionLevel, - ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem), + ItemFilter = (Item potentialItem) => + { + return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _); + }, ItemCount = ItemCount, TakeWholeStack = MoveWholeStack }, onAbandon: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index a41303c20..d8cfdf3fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -9,11 +9,12 @@ namespace Barotrauma class AIObjectiveDecontainItem : AIObjective { public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier(); + public override bool AllowWhileHandcuffed => false; public Func GetItemPriority; //can either be a tag or an identifier - private readonly string[] itemIdentifiers; + private readonly Identifier[] itemIdentifiers; private readonly ItemContainer sourceContainer; private readonly ItemContainer targetContainer; private readonly Item targetItem; @@ -52,16 +53,16 @@ namespace Barotrauma this.targetContainer = targetContainer; } - public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) - : this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { } + public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) + : this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { } - public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) + public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; for (int i = 0; i < itemIdentifiers.Length; i++) { - itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); + itemIdentifiers[i] = itemIdentifiers[i]; } this.sourceContainer = sourceContainer; this.targetContainer = targetContainer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index 5a57adc31..ed5408fb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -88,7 +88,7 @@ namespace Barotrauma escapeProgress += Rand.Range(2, 5); if (escapeProgress > 15) { - Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier()); + Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem); if (handcuffs != null) { handcuffs.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index cec282b90..7cd20e569 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -15,6 +15,8 @@ namespace Barotrauma public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; + private readonly Hull targetHull; private AIObjectiveGetItem getExtinguisherObjective; @@ -30,8 +32,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } bool isOrder = objectiveManager.HasOrder(); @@ -176,19 +177,19 @@ namespace Barotrauma getExtinguisherObjective = null; gotoObjective = null; sinTime = 0; - SteeringManager.Reset(); + SteeringManager?.Reset(); } protected override void OnCompleted() { base.OnCompleted(); - SteeringManager.Reset(); + SteeringManager?.Reset(); } protected override void OnAbandon() { base.OnAbandon(); - SteeringManager.Reset(); + SteeringManager?.Reset(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 3cb129c96..19fb5d725 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AllowWhileHandcuffed => false; private readonly Identifier gearTag; @@ -22,34 +23,20 @@ namespace Barotrauma public const float MIN_OXYGEN = 10; - public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier(); - public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier(); - /// - /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) - /// - public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier(); - public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier(); - protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head); public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR; + gearTag = needsDivingSuit ? Tags.HeavyDivingGear : Tags.LightDivingGear; } protected override void Act(float deltaTime) { - if (character.LockHands) - { - Abandon = true; - return; - } - TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true)); - if (targetItem == null && gearTag == LIGHT_DIVING_GEAR) + if (targetItem == null && gearTag == Tags.LightDivingGear) { - TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true)); + TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true)); } if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && @@ -74,7 +61,7 @@ namespace Barotrauma onCompleted: () => { RemoveSubObjective(ref getDivingGear); - if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable masks, requireEquipped: true)) { foreach (Item mask in masks) { @@ -95,10 +82,10 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) + if (HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: min)) { character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); - if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1) + if (character.Inventory.FindAllItems(i => i.HasTag(Tags.OxygenSource) && i.Condition > min, recursive: true).Count == 1) { character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); } @@ -109,7 +96,7 @@ namespace Barotrauma } } var container = targetItem.GetComponent(); - var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + var objective = new AIObjectiveContainItem(character, Tags.OxygenSource, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, @@ -119,7 +106,7 @@ namespace Barotrauma }; if (container.HasSubContainers) { - objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + objective.TargetSlot = container.FindSuitableSubContainerIndex(Tags.OxygenSource); } // Only remove the oxygen source being replaced objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i); @@ -132,7 +119,7 @@ namespace Barotrauma // Try to seek any oxygen sources, even if they have minimal amount of oxygen. TryAddSubObjective(ref getOxygen, () => { - return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + return new AIObjectiveContainItem(character, Tags.OxygenSource, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, @@ -142,7 +129,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f)) + if (remainingTanks > 0 && !HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: 0.01f)) { character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f); } @@ -158,7 +145,7 @@ namespace Barotrauma int ReportOxygenTankCount() { if (character.Submarine != Submarine.MainSub) { return 1; } - int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f); @@ -177,7 +164,7 @@ namespace Barotrauma { return item != null && - item.HasTag(OXYGEN_SOURCE) && + item.HasTag(Tags.OxygenSource) && item.Condition > 0 && (oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value)); } @@ -188,7 +175,7 @@ namespace Barotrauma targetItem = item; if (targetItem != null) { - oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(Tags.OxygenSource); } else { @@ -212,7 +199,7 @@ namespace Barotrauma // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks. float min = 0.01f; float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min; - if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag("oxygensource") && i.ConditionPercentage >= minOxygen)) + if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen)) { // There's a valid oxygen tank in the inventory -> no need to swap the tank too early. minOxygen = min; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index a35ec388d..a9abe7d30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -40,24 +40,21 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed) - { - Priority = 0; - return Priority; - } if (character.CurrentHull == null) { Priority = ( objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasOrder(o => o.Priority > 0) || objectiveManager.HasActiveObjective() || - objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0)) && ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10; } else { - if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || - NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character))) + bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); + static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true); + if (isSuffocatingInDivingSuit || + NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) || + (!objectiveManager.HasActiveObjective() && IsSuffocatingWithoutDivingGear(character))) { Priority = AIObjectiveManager.MaxObjectivePriority; } @@ -215,7 +212,7 @@ namespace Barotrauma AllowGoingOutside = character.IsProtectedFromPressure || character.CurrentHull == null || - character.CurrentHull.IsTaggedAirlock() || + character.CurrentHull.IsAirlock || character.CurrentHull.LeadsOutside(character) }, onCompleted: () => @@ -258,6 +255,13 @@ namespace Barotrauma } if (subObjectives.Any(so => so.CanBeCompleted)) { return; } UpdateSimpleEscape(deltaTime); + if (cannotFindSafeHull && !character.IsInFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) + { + if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab)) + { + objectiveManager.AddObjective(new AIObjectiveReturn(character, character, objectiveManager)); + } + } } } @@ -433,8 +437,7 @@ namespace Barotrauma } else { - // TODO: could also target gaps that get us inside? - if (potentialHull.IsTaggedAirlock()) + if (potentialHull.IsAirlock) { hullSafety = 100; hullIsAirlock = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index f08a264c6..b4c28bfb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -12,7 +12,9 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + public override bool AllowInFriendlySubs => true; public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; public Gap Leak { get; private set; } @@ -35,8 +37,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } float coopMultiplier = 1; @@ -94,6 +95,7 @@ namespace Barotrauma protected override void Act(float deltaTime) { var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true); + var repairTool = weldingTool?.GetComponent(); if (weldingTool == null) { TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), @@ -110,17 +112,25 @@ namespace Barotrauma } else { - if (weldingTool.OwnInventory == null) + if (repairTool == null) { #if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no RepairTool component but is tagged as a welding tool"); #endif Abandon = true; return; } - if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) + if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) +#if DEBUG + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory"); +#endif + Abandon = true; + return; + } + if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 0.0f)) + { + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, Tags.WeldingFuel, weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { RemoveExisting = true }, @@ -138,7 +148,7 @@ namespace Barotrauma void ReportWeldingFuelTankCount() { if (character.Submarine != Submarine.MainSub) { return; } - int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f); @@ -152,15 +162,6 @@ namespace Barotrauma } } if (subObjectives.Any()) { return; } - var repairTool = weldingTool.GetComponent(); - if (repairTool == null) - { -#if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool"); -#endif - Abandon = true; - return; - } Vector2 toLeak = Leak.WorldPosition - character.AnimController.AimSourceWorldPos; // TODO: use the collider size/reach? if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150) @@ -200,7 +201,8 @@ namespace Barotrauma Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), endNodeFilter = IsSuitableEndNode, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) - SpeakCannotReachCondition = () => !CheckObjectiveSpecific() + // Only report about contextual targets. + SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific() }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index d9c8ff24a..dff7dc2ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -9,7 +9,8 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool AllowInAnySub => true; + public override bool AllowInFriendlySubs => true; + private Hull PrioritizedHull { get; set; } public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index aaee84fa1..1ed1fe400 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -15,6 +15,7 @@ namespace Barotrauma public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowMultipleInstances => true; + public override bool AllowWhileHandcuffed => false; public HashSet ignoredItems = new HashSet(); @@ -158,11 +159,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands) - { - Abandon = true; - return; - } if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -271,15 +267,49 @@ namespace Barotrauma Inventory itemInventory = targetItem.ParentInventory; var slots = itemInventory?.FindIndices(targetItem); + var droppedStack = TargetItem.DroppedStack.ToList(); if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags)) { - if (TakeWholeStack && slots != null) + if (TakeWholeStack) { - foreach (int slot in slots) + //taking the whole stack in this context means "as many items that can fit in one of the bot's slots", + //and the stack means either a stack of items in an inventory slot or a "dropped stack" + //so we need a bit of extra logic here + int maxStackSize = 0; + int takenItemCount = 1; + for (int i = 0; i < character.Inventory.Capacity; i++) { - foreach (Item item in itemInventory.GetItemsAt(slot).ToList()) + maxStackSize = Math.Max(maxStackSize, character.Inventory.HowManyCanBePut(targetItem.Prefab, i, condition: null)); + } + if (slots != null) + { + foreach (int slot in slots) { - HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true); + foreach (Item item in itemInventory.GetItemsAt(slot).ToList()) + { + if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true)) + { + takenItemCount++; + if (takenItemCount >= maxStackSize) { break; } + } + else + { + break; + } + } + } + } + foreach (var item in droppedStack) + { + if (item == TargetItem) { continue; } + if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true)) + { + takenItemCount++; + if (takenItemCount >= maxStackSize) { break; } + } + else + { + break; } } } @@ -411,7 +441,7 @@ namespace Barotrauma if (!CheckItem(item)) { continue; } if (item.Container != null) { - if (item.Container.HasTag("donttakeitems")) { continue; } + if (item.Container.HasTag(Tags.DontTakeItems)) { continue; } if (ignoredItems.Contains(item.Container)) { continue; } if (ignoredContainerIdentifiers != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 5355767d1..5c909a4a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool AllowMultipleInstances => true; + public override bool AllowWhileHandcuffed => false; public bool AllowStealing { get; set; } public bool TakeWholeStack { get; set; } @@ -40,55 +41,48 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands) + if (subObjectivesCreated) { return; } + foreach (Identifier tag in gearTags) { - Abandon = true; - return; - } - if (!subObjectivesCreated) - { - foreach (Identifier tag in gearTags) - { - if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } - int count = gearTags.Count(t => t == tag); - AIObjectiveGetItem? getItem = null; - TryAddSubObjective(ref getItem, () => - new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) - { - AllowVariants = AllowVariants, - Wear = Wear, - TakeWholeStack = TakeWholeStack, - AllowStealing = AllowStealing, - ignoredIdentifiersOrTags = ignoredTags, - CheckPathForEachItem = CheckPathForEachItem, - RequireNonEmpty = RequireNonEmpty, - ItemCount = count, - SpeakIfFails = RequireAllItems - }, - onCompleted: () => - { - var item = getItem?.TargetItem; - if (item?.IsOwnedBy(character) != null) - { - achievedItems.Add(item); - } - }, - onAbandon: () => - { - var item = getItem?.TargetItem; - if (item != null) - { - achievedItems.Remove(item); - } - RemoveSubObjective(ref getItem); - if (RequireAllItems) - { - Abandon = true; - } - }); - } - subObjectivesCreated = true; + if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } + int count = gearTags.Count(t => t == tag); + AIObjectiveGetItem? getItem = null; + TryAddSubObjective(ref getItem, () => + new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) + { + AllowVariants = AllowVariants, + Wear = Wear, + TakeWholeStack = TakeWholeStack, + AllowStealing = AllowStealing, + ignoredIdentifiersOrTags = ignoredTags, + CheckPathForEachItem = CheckPathForEachItem, + RequireNonEmpty = RequireNonEmpty, + ItemCount = count, + SpeakIfFails = RequireAllItems + }, + onCompleted: () => + { + var item = getItem?.TargetItem; + if (item?.IsOwnedBy(character) != null) + { + achievedItems.Add(item); + } + }, + onAbandon: () => + { + var item = getItem?.TargetItem; + if (item != null) + { + achievedItems.Remove(item); + } + RemoveSubObjective(ref getItem); + if (RequireAllItems) + { + Abandon = true; + } + }); } + subObjectivesCreated = true; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index dfc1966bf..c5b039824 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -223,23 +223,30 @@ namespace Barotrauma Hull targetHull = GetTargetHull(); if (!IsFollowOrder) { + // Abandon if going through unsafe paths or targeting unsafe hulls. bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull); if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls) { - if (HumanAIController.UnsafeHulls.Contains(targetHull)) + // Wait orders check this so that the bot temporarily leaves the unsafe hull. + // Non-orders (that are not set to ignore the unsafe hulls) abandon. In practice this means e.g. repair and clean up item subobjectives (of the looping parent objective). + // Other orders are only abandoned if the hull is unreachable, because the path is invalid or not found at all. + if (IsWaitOrder || !objectiveManager.HasOrders()) { - isUnreachable = true; - HumanAIController.AskToRecalculateHullSafety(targetHull); - } - else if (PathSteering?.CurrentPath != null) - { - foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) + if (HumanAIController.UnsafeHulls.Contains(targetHull)) { - if (wp.CurrentHull == null) { continue; } - if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(targetHull); + } + else if (PathSteering?.CurrentPath != null) + { + foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) { - isUnreachable = true; - HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + if (wp.CurrentHull == null) { continue; } + if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + { + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + } } } } @@ -803,7 +810,7 @@ namespace Barotrauma private void StopMovement() { - SteeringManager.Reset(); + SteeringManager?.Reset(); if (Target != null) { character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index bb0c590c2..bd01ecebf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -382,7 +382,9 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; } + if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; } + //not possible in vanilla game, but a mod might have holdable/attachable chairs + if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; } var controller = item.GetComponent(); if (controller == null || controller.User != null) { continue; } item.TryInteract(character, forceSelectKey: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 118849867..74ac238a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -17,6 +17,8 @@ namespace Barotrauma set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } + public override bool AllowWhileHandcuffed => false; + private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } private Item Container { get; } private ItemContainer ItemContainer { get; } @@ -161,8 +163,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition)) @@ -299,7 +300,7 @@ namespace Barotrauma if (rootInventoryOwner is Character owner && owner != character) { return false; } if (rootInventoryOwner is Item parentItem) { - if (parentItem.HasTag("donttakeitems")) { return false; } + if (parentItem.HasTag(Tags.DontTakeItems)) { return false; } } if (!item.HasAccess(character)) { return false; } if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 8edac36a0..5419eedf7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -44,6 +44,8 @@ namespace Barotrauma public override bool CanBeCompleted => true; public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowSubObjectiveSorting => true; + public override bool AllowWhileHandcuffed => false; + public virtual bool InverseTargetEvaluation => false; protected virtual bool ResetWhenClearingIgnoreList => true; protected virtual bool ForceOrderPriority => true; @@ -117,51 +119,44 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; + HandleNonAllowed(); return Priority; } - if (character.LockHands) + // Allow the target value to be more than 100. + float targetValue = TargetEvaluation(); + if (InverseTargetEvaluation) + { + targetValue = 100 - targetValue; + } + var currentSubObjective = CurrentSubObjective; + if (currentSubObjective != null && currentSubObjective.Priority > targetValue) + { + // If the priority is higher than the target value, let's just use it. + // The priority calculation is more precise, but it takes into account things like distances, + // so it's better not to use it if it's lower than the rougher targetValue. + targetValue = currentSubObjective.Priority; + } + // If the target value is less than 1% of the max value, let's just treat it as zero. + if (targetValue < 1) { Priority = 0; } else { - // Allow the target value to be more than 100. - float targetValue = TargetEvaluation(); - if (InverseTargetEvaluation) + if (objectiveManager.IsOrder(this)) { - targetValue = 100 - targetValue; - } - var currentSubObjective = CurrentSubObjective; - if (currentSubObjective != null && currentSubObjective.Priority > targetValue) - { - // If the priority is higher than the target value, let's just use it. - // The priority calculation is more precise, but it takes into account things like distances, - // so it's better not to use it if it's lower than the rougher targetValue. - targetValue = currentSubObjective.Priority; - } - // If the target value is less than 1% of the max value, let's just treat it as zero. - if (targetValue < 1) - { - Priority = 0; + Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; } else { - if (objectiveManager.IsOrder(this)) + float max = AIObjectiveManager.LowestOrderPriority - 1; + if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character)) { - Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; - } - else - { - float max = AIObjectiveManager.LowestOrderPriority - 1; - if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character)) - { - // Allow higher prio - max = AIObjectiveManager.EmergencyObjectivePriority; - } - float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); - Priority = MathHelper.Lerp(0, max, value); + // Allow higher prio + max = AIObjectiveManager.EmergencyObjectivePriority; } + float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); + Priority = MathHelper.Lerp(0, max, value); } } return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index af05c7095..40be336b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -530,7 +530,7 @@ namespace Barotrauma case "cleanupitems": if (order.TargetEntity is Item targetItem) { - if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null) + if (targetItem.HasTag(Tags.AllowCleanup) && targetItem.ParentInventory == null && targetItem.OwnInventory != null) { // Target all items inside the container newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 163b7bee2..2e35e6ff0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -14,6 +14,7 @@ namespace Barotrauma public override bool AllowAutomaticItemUnequipping => true; public override bool AllowMultipleInstances => true; public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret); private readonly ItemComponent component, controller; @@ -47,10 +48,9 @@ namespace Barotrauma protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); - if (!IsAllowed || character.LockHands) + if (!IsAllowed) { - Priority = 0; - Abandon = !isOrder; + HandleNonAllowed(); return Priority; } if (!isOrder && component.Item.ConditionPercentage <= 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index b46f1f2e4..b5515d5de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -13,6 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool KeepDivingGearOnAlsoWhenInactive => true; public override bool PrioritizeIfSubObjectivesActive => true; + public override bool AllowWhileHandcuffed => false; private AIObjectiveGetItem getSingleItemObjective; private AIObjectiveGetItems getAllItemsObjective; @@ -60,8 +61,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } Priority = objectiveManager.GetOrderPriority(this); @@ -75,11 +75,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands) - { - Abandon = true; - return; - } if (!subObjectivesCreated) { if (FindAllItems && targetItem == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 72d84b107..80392c641 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "pump water".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; + public override bool AllowWhileHandcuffed => false; private List pumpList; @@ -54,7 +55,7 @@ namespace Barotrauma var pump = item.GetComponent(); if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; } if (pump.Item.Submarine.TeamID != character.TeamID) { continue; } - if (pump.Item.HasTag("ballast")) { continue; } + if (pump.Item.HasTag(Tags.Ballast)) { continue; } pumpList.Add(pump); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 9c06fb8b2..81197c7e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -10,8 +10,9 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); - public override bool AllowInAnySub => true; + public override bool AllowInFriendlySubs => true; public override bool KeepDivingGearOn => Item?.CurrentHull == null; + public override bool AllowWhileHandcuffed => false; public Item Item { get; private set; } @@ -36,10 +37,13 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed || Item.IgnoreByAI(character)) + if (!IsAllowed) { HandleNonAllowed(); } + if (Item.IgnoreByAI(character)) { - Priority = 0; Abandon = true; + } + if (Abandon) + { if (IsRepairing()) { Item.Repairables.ForEach(r => r.StopRepairing(character)); @@ -136,32 +140,35 @@ namespace Barotrauma } if (repairTool != null) { - if (repairTool.Item.OwnInventory == null) + if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) { -#if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory"); -#endif - Abandon = true; - return; - } - RelatedItem item = null; - Item fuel = null; - foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained]) - { - item = requiredItem; - fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); - if (fuel != null) { break; } - } - if (fuel == null) - { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + if (repairTool.Item.OwnInventory == null) { - RemoveExisting = true - }, - onCompleted: () => RemoveSubObjective(ref refuelObjective), - onAbandon: () => Abandon = true); - return; +#if DEBUG + DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"{repairTool}\" has no proper inventory."); +#endif + Abandon = true; + return; + } + RelatedItem item = null; + Item fuel = null; + foreach (RelatedItem requiredItem in requiredItems) + { + item = requiredItem; + fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); + if (fuel != null) { break; } + } + if (fuel == null) + { + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + { + RemoveExisting = true + }, + onCompleted: () => RemoveSubObjective(ref refuelObjective), + onAbandon: () => Abandon = true); + return; + } } } if (character.CanInteractWith(Item, out _, checkLinked: false)) @@ -170,10 +177,8 @@ namespace Barotrauma if (waitTimer < WaitTimeBeforeRepair) { return; } HumanAIController.FaceTarget(Item); - if (repairTool != null) - { - OperateRepairTool(deltaTime); - } + + bool repairThroughRepairInterface = false; foreach (Repairable repairable in Item.Repairables) { if (repairable.CurrentFixer != null && repairable.CurrentFixer != character) @@ -185,10 +190,11 @@ namespace Barotrauma { if (character.SelectedItem != Item) { - if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) || - Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true)) + if (Item.TryInteract(character, forceUseKey: true) || + Item.TryInteract(character, forceSelectKey: true)) { character.SelectedItem = Item; + repairThroughRepairInterface = true; } else { @@ -209,8 +215,17 @@ namespace Barotrauma { repairable.StartRepairing(character, Repairable.FixActions.Repair); } + else + { + repairThroughRepairInterface = true; + } break; } + + if (!repairThroughRepairInterface && repairTool != null && !Abandon) + { + OperateRepairTool(deltaTime); + } } else { @@ -222,7 +237,8 @@ namespace Barotrauma { var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { - TargetName = Item.Name + TargetName = Item.Name, + SpeakCannotReachCondition = () => isPriority }; if (repairTool != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index dbb0a68a9..3e10ab620 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -19,7 +19,7 @@ namespace Barotrauma public Item PrioritizedItem { get; private set; } public override bool AllowMultipleInstances => true; - public override bool AllowInAnySub => true; + public override bool AllowInFriendlySubs => true; public readonly static float RequiredSuccessFactor = 0.4f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index cba0a97f4..14963a008 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -16,12 +16,13 @@ namespace Barotrauma public override bool AllowOutsideSubmarine => true; public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; const float TreatmentDelay = 0.5f; const float CloseEnoughToTreat = 100.0f; - private readonly Character targetCharacter; + public readonly Character Target; private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem replaceOxygenObjective; @@ -44,7 +45,7 @@ namespace Barotrauma Abandon = true; return; } - this.targetCharacter = targetCharacter; + Target = targetCharacter; } protected override void OnAbandon() @@ -61,55 +62,55 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead) + if (Target == null || Target.Removed || Target.IsDead) { Abandon = true; return; } - var otherRescuer = targetCharacter.SelectedBy; + var otherRescuer = Target.SelectedBy; if (otherRescuer != null && otherRescuer != character) { // Someone else is rescuing/holding the target. Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical"); return; } - if (targetCharacter != character) + if (Target != character) { - if (targetCharacter.IsIncapacitated) + if (Target.IsIncapacitated) { // Check if the character needs more oxygen - if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter)) + if (!ignoreOxygen && character.SelectedCharacter == Target || character.CanInteractWith(Target)) { // Replace empty oxygen and welding fuel. - if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable suits, requireEquipped: true)) + if (HumanAIController.HasItem(Target, Tags.HeavyDivingGear, out IEnumerable suits, requireEquipped: true)) { Item suit = suits.FirstOrDefault(); if (suit != null) { AIController.UnequipEmptyItems(character, suit); - AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel")); + AIController.UnequipContainedItems(character, suit, it => it.HasTag(Tags.WeldingFuel)); } } - else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + else if (HumanAIController.HasItem(Target, Tags.LightDivingGear, out IEnumerable masks, requireEquipped: true)) { Item mask = masks.FirstOrDefault(); if (mask != null) { AIController.UnequipEmptyItems(character, mask); - AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel")); + AIController.UnequipContainedItems(character, mask, it => it.HasTag(Tags.WeldingFuel)); } } - bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0; + bool ShouldRemoveDivingSuit() => Target.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && Target.CurrentHull?.LethalPressure <= 0; if (ShouldRemoveDivingSuit()) { suits.ForEach(suit => suit.Drop(character)); } - else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0))) + else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(Tags.OxygenSource) && it.ConditionPercentage > 0))) { // The target has a suit equipped with an empty oxygen tank. // Can't remove the suit, because the target needs it. // If we happen to have an extra oxygen tank in the inventory, let's swap it. - Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character); + Item spareOxygenTank = FindOxygenTank(Target) ?? FindOxygenTank(character); if (spareOxygenTank != null) { Item suit = suits.FirstOrDefault(); @@ -133,36 +134,36 @@ namespace Barotrauma Item FindOxygenTank(Character c) => c.Inventory.FindItem(i => - i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && + i.HasTag(Tags.OxygenSource) && i.ConditionPercentage > 1 && - i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null, + i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag(Tags.DivingGear)) == null, recursive: true); } } - if (character.Submarine != null && targetCharacter.CurrentHull != null) + if (character.Submarine != null && Target.CurrentHull != null) { - if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + if (HumanAIController.GetHullSafety(Target.CurrentHull, Target) < HumanAIController.HULL_SAFETY_THRESHOLD) { // Incapacitated target is not in a safe place -> Move to a safe place first - if (character.SelectedCharacter != targetCharacter) + if (character.SelectedCharacter != Target) { - if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", - ("[targetname]", targetCharacter.Name, FormatCapitals.No), - ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, - null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f); + ("[targetname]", Target.Name, FormatCapitals.No), + ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f); } // Go to the target and select it - if (!character.CanInteractWith(targetCharacter)) + if (!character.CanInteractWith(Target)) { RemoveSubObjective(ref replaceOxygenObjective); RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), - TargetName = targetCharacter.DisplayName + TargetName = Target.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), onAbandon: () => @@ -173,7 +174,7 @@ namespace Barotrauma } else { - character.SelectCharacter(targetCharacter); + character.SelectCharacter(Target); } } else @@ -213,16 +214,16 @@ namespace Barotrauma if (subObjectives.Any()) { return; } - if (targetCharacter != character && !character.CanInteractWith(targetCharacter)) + if (Target != character && !character.CanInteractWith(Target)) { RemoveSubObjective(ref replaceOxygenObjective); RemoveSubObjective(ref goToObjective); // Go to the target and select it - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), - TargetName = targetCharacter.DisplayName + TargetName = Target.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), onAbandon: () => @@ -234,14 +235,14 @@ namespace Barotrauma else { // We can start applying treatment - if (character != targetCharacter && character.SelectedCharacter != targetCharacter) + if (character != Target && character.SelectedCharacter != Target) { - if (targetCharacter.CurrentHull?.DisplayName != null) + if (Target.CurrentHull?.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", - ("[targetname]", targetCharacter.Name, FormatCapitals.No), - ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, - null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f); + ("[targetname]", Target.Name, FormatCapitals.No), + ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f); } } GiveTreatment(deltaTime); @@ -253,7 +254,7 @@ namespace Barotrauma private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { - if (targetCharacter == null) + if (Target == null) { string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!"; DebugConsole.ThrowError(errorMsg); @@ -263,10 +264,10 @@ namespace Barotrauma SteeringManager.Reset(); - if (!targetCharacter.IsPlayer) + if (!Target.IsPlayer) { // If the target is a bot, don't let it move - targetCharacter.AIController?.SteeringManager?.Reset(); + Target.AIController?.SteeringManager?.Reset(); } if (treatmentTimer > 0.0f) { @@ -275,28 +276,28 @@ namespace Barotrauma } treatmentTimer = TreatmentDelay; - float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f; + float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f; //find which treatments are the most suitable to treat the character's current condition - targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); + Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); //check if we already have a suitable treatment for any of the afflictions - foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) + foreach (Affliction affliction in GetSortedAfflictions(Target)) { if (affliction == null) { throw new Exception("Affliction was null"); } if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } float bestSuitability = 0.0f; Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitabilities) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) { Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); //allow taking items from the target's inventory too if the target is unconscious - if (matchingItem == null && targetCharacter.IsIncapacitated) + if (matchingItem == null && Target.IsIncapacitated) { - matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); + matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); } if (matchingItem != null) { @@ -307,7 +308,7 @@ namespace Barotrauma } if (bestItem != null) { - if (targetCharacter != character) { character.SelectCharacter(targetCharacter); } + if (Target != character) { character.SelectCharacter(Target); } ApplyTreatment(affliction, bestItem); //wait a bit longer after applying a treatment to wait for potential side-effects to manifest treatmentTimer = TreatmentDelay * 4; @@ -370,12 +371,12 @@ namespace Barotrauma ("[treatment1]", itemListStr), ("[treatment2]", itemNameList.Last())); } - if (targetCharacter != character && character.IsOnPlayerTeam) + if (Target != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", - ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[targetname]", Target.Name, FormatCapitals.No), ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value, - null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f); + null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f); } RemoveSubObjective(ref getItemObjective); TryAddSubObjective(ref getItemObjective, @@ -397,18 +398,18 @@ namespace Barotrauma } } } - else if (!targetCharacter.IsUnconscious) + else if (!Target.IsUnconscious) { Abandon = true; //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) SpeakCannotTreat(); return; } - if (character != targetCharacter) + if (character != Target) { if (cprSuitability > 0.0f) { - character.SelectCharacter(targetCharacter); + character.SelectCharacter(Target); character.AnimController.Anim = AnimController.Animation.CPR; performedCpr = true; } @@ -421,40 +422,40 @@ namespace Barotrauma private void SpeakCannotTreat() { - LocalizedString msg = character == targetCharacter ? + LocalizedString msg = character == Target ? TextManager.Get("dialogcannottreatself") : - TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No); + TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", Target.DisplayName, FormatCapitals.No); character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); } private void ApplyTreatment(Affliction affliction, Item item) { - item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); + item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction)); } protected override bool CheckObjectiveSpecific() { - bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); - if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) + bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target); + if (isCompleted && Target != character && character.IsOnPlayerTeam) { string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed"; - string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value; - character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); + string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value; + character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } return isCompleted; } protected override float GetPriority() { - if (!IsAllowed || targetCharacter == null) + if (Target == null) { Abandon = true; } + if (!IsAllowed) { HandleNonAllowed(); } + if (Abandon) { - Priority = 0; - Abandon = true; return Priority; } if (character.CurrentHull != null) { - if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) + if (Character.CharacterList.Any(c => c.CurrentHull == Target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { // Don't go into rooms that have enemies Priority = 0; @@ -462,18 +463,18 @@ namespace Barotrauma return Priority; } } - float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); + float horizontalDistance = Math.Abs(character.WorldPosition.X - Target.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - Target.WorldPosition.Y); if (character.Submarine?.Info is { IsRuin: false }) { verticalDistance *= 2; } float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); - if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull) + if (character.CurrentHull != null && Target.CurrentHull == character.CurrentHull) { distanceFactor = 1; } - float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; + float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(Target) / 100; float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 97ce2855f..4bfadb3f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -34,24 +34,29 @@ namespace Barotrauma protected override bool Filter(Character target) { - if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; } - if (GetTreatableAfflictions(target).Any()) + if (!IsValidTarget(target, character, out bool ignoredasMinorWounds)) { - return true; - } - else - { - //the target might be at a low enough health to be considered a valid target, - //but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them - // -> make the bot speak to make it clear the bot intentionally ignores very minor injuries - if (!charactersWithMinorInjuries.Contains(character)) + if (ignoredasMinorWounds) { - character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value, - null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f); - charactersWithMinorInjuries.Add(character); + //the target might be at a low enough health to be considered a valid target, + //but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them + // -> make the bot speak to make it clear the bot intentionally ignores very minor injuries + if (character.IsOnPlayerTeam && target != character && !charactersWithMinorInjuries.Contains(target)) + { + // But only speak about targets when we are not already actively treating, in which case we should be speaking about the current target. + if (objectiveManager.GetFirstActiveObjective() == null) + { + charactersWithMinorInjuries.Add(target); + character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value, + delay: 1.0f, + identifier: $"notreatableafflictions{target.Name}".ToIdentifier(), + minDurationBetweenSimilar: 10.0f); + } + } } return false; - } + } + return true; } protected override IEnumerable GetList() => Character.CharacterList; @@ -103,12 +108,13 @@ namespace Barotrauma return Math.Clamp(vitality, 0, 100); } - public static IEnumerable GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false) + public static IEnumerable GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold) { var allAfflictions = character.CharacterHealth.GetAllAfflictions(); foreach (Affliction affliction in allAfflictions) { if (affliction.Prefab.IsBuff) { continue; } + if (!affliction.Prefab.HasTreatments) { continue; } if (!ignoreTreatmentThreshold) { //other afflictions of the same type increase the "treatability" @@ -116,7 +122,6 @@ namespace Barotrauma float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction); if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } } - if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; } if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; } @@ -128,39 +133,50 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Character target) => HumanAIController.RemoveTargets(character, target); - public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true) + public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds) { + ignoredAsMinorWounds = false; if (target == null || target.IsDead || target.Removed) { return false; } if (target.IsInstigator) { return false; } if (target.IsPet) { return false; } if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } + bool isBelowTreatmentThreshold; + float vitalityFactor; if (character.AIController is HumanAIController humanAI) { - if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) - { - return false; - } - if (!humanAI.ObjectiveManager.HasOrder()) - { - if (!character.IsMedic && target != character) - { - // Don't allow to treat others autonomously, unless we are a medic - return false; - } - // Ignore unsafe hulls, unless ordered - if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) - { - return false; - } - } - if (requireTreatableAfflictions && GetTreatableAfflictions(target).None()) - { - return false; - } + if (!IsValidTargetForAI(target, humanAI)) { return false; } + vitalityFactor = GetVitalityFactor(target); + isBelowTreatmentThreshold = vitalityFactor < GetVitalityThreshold(humanAI.ObjectiveManager, character, target); } else { - if (GetVitalityFactor(target) >= vitalityThreshold) { return false; } + vitalityFactor = GetVitalityFactor(target); + isBelowTreatmentThreshold = vitalityFactor < vitalityThreshold; + } + bool hasTreatableAfflictions = GetTreatableAfflictions(target, ignoreTreatmentThreshold: false).Any(); + bool isValidTarget = isBelowTreatmentThreshold && hasTreatableAfflictions; + if (!isValidTarget) + { + ignoredAsMinorWounds = hasTreatableAfflictions || vitalityFactor < 100; + } + return isValidTarget; + } + + private static bool IsValidTargetForAI(Character target, HumanAIController humanAI) + { + Character character = humanAI.Character; + if (!humanAI.ObjectiveManager.HasOrder()) + { + if (!character.IsMedic && target != character) + { + // Don't allow to treat others autonomously, unless we are a medic + return false; + } + // Ignore unsafe hulls, unless ordered + if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) + { + return false; + } } if (character.Submarine != null) { @@ -189,11 +205,5 @@ namespace Barotrauma } return character.GetDamageDoneByAttacker(target) <= 0; } - - public override void Reset() - { - base.Reset(); - charactersWithMinorInjuries.Clear(); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index 14fc0d495..ee7109ff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -20,7 +20,7 @@ namespace Barotrauma ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); if (ReturnTarget == null) { - DebugConsole.ThrowError("Error with a Return objective: no suitable return target found"); + DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found"); Abandon = true; } @@ -47,8 +47,7 @@ namespace Barotrauma } else { - // TODO: Consider if this needs to be addressed - Priority = 0; + Priority = AIObjectiveManager.LowestOrderPriority - 1; } return Priority; } @@ -91,7 +90,7 @@ namespace Barotrauma targetHull = d.Item.CurrentHull; break; } - if (targetHull != null && !targetHull.IsTaggedAirlock()) + if (targetHull != null && !targetHull.IsAirlock) { // Target the closest airlock float closestDist = 0; @@ -99,7 +98,7 @@ namespace Barotrauma foreach (Hull hull in Hull.HullList) { if (hull.Submarine != targetHull.Submarine) { continue; } - if (!hull.IsTaggedAirlock()) { continue; } + if (!hull.IsAirlock) { continue; } float dist = Vector2.DistanceSquared(targetHull.Position, hull.Position); if (airlock == null || closestDist <= 0 || dist < closestDist) { @@ -146,7 +145,7 @@ namespace Barotrauma bool targetIsAirlock = false; foreach (var hull in ReturnTarget.GetHulls(false)) { - bool hullIsAirlock = hull.IsTaggedAirlock(); + bool hullIsAirlock = hull.IsAirlock; if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character))) { float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 0b48320e7..21c5aeb35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -85,6 +85,12 @@ namespace Barotrauma public readonly bool TargetAllCharacters; public bool IsReport => TargetAllCharacters && !MustSetTarget; + public bool IsVisibleAsReportButton => + IsReport && !Hidden && SymbolSprite != null && + (!TraitorModeOnly || GameMain.GameSession is { TraitorsEnabled: true }); + + public bool TraitorModeOnly; + public bool IsDismissal => Identifier == DismissalIdentifier; public readonly float FadeOutTime; @@ -172,6 +178,7 @@ namespace Barotrauma ControllerTags = orderElement.GetAttributeIdentifierArray("controllertags", Array.Empty()).ToImmutableArray(); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeIdentifierArray("appropriatejobs", Array.Empty()).ToImmutableArray(); + TraitorModeOnly = orderElement.GetAttributeBool("TraitorModeOnly", false); PreferredJobs = orderElement.GetAttributeIdentifierArray("preferredjobs", Array.Empty()).ToImmutableArray(); Options = orderElement.GetAttributeIdentifierArray("options", Array.Empty()).ToImmutableArray(); HiddenOptions = orderElement.GetAttributeIdentifierArray("hiddenoptions", Array.Empty()).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 4f64dac4d..d5f90c438 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -34,7 +34,23 @@ namespace Barotrauma set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); } } + /// + /// At which point is the pet considered "unhappy" (playing unhappy sounds and showing the icon) + /// + public float UnhappyThreshold { get; set; } + + /// + /// At which point is the pet considered "happy" (playing happy sounds and showing the icon) + /// + public float HappyThreshold { get; set; } + public float MaxHappiness { get; set; } + + + /// + /// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon) + /// + public float HungryThreshold { get; set; } public float MaxHunger { get; set; } public float HappinessDecreaseRate { get; set; } @@ -43,7 +59,7 @@ namespace Barotrauma public float PlayForce { get; set; } public float PlayTimer { get; set; } - private float? unstunY { get; set; } + private float? UnstunY { get; set; } public EnemyAIController AIController { get; private set; } = null; @@ -136,7 +152,7 @@ namespace Barotrauma if (aggregate >= r && Items[i].Prefab != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); - Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); + Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); break; } } @@ -164,14 +180,18 @@ namespace Barotrauma AIController = aiController; AIController.Character.CanBeDragged = true; - MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); - MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); + MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f); + UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f); + HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f); + + MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f); + HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f); Happiness = MaxHappiness * 0.5f; Hunger = MaxHunger * 0.5f; - HappinessDecreaseRate = element.GetAttributeFloat("happinessdecreaserate", 0.1f); - HungerIncreaseRate = element.GetAttributeFloat("hungerincreaserate", 0.25f); + HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f); + HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f); PlayForce = element.GetAttributeFloat("playforce", 15.0f); @@ -208,9 +228,9 @@ namespace Barotrauma public StatusIndicatorType GetCurrentStatusIndicatorType() { - if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; } - if (Happiness > MaxHappiness * 0.8f) { return StatusIndicatorType.Happy; } - if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; } + if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; } + if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; } + if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; } return StatusIndicatorType.None; } @@ -264,12 +284,12 @@ namespace Barotrauma public void Play(Character player) { if (PlayTimer > 0.0f) { return; } - if (Owner == null) { Owner = player; } + Owner ??= player; PlayTimer = 5.0f; AIController.Character.IsRagdolled = true; Happiness += 10.0f; AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); - unstunY = AIController.Character.SimPosition.Y; + UnstunY = AIController.Character.SimPosition.Y; #if CLIENT AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); #endif @@ -297,16 +317,16 @@ namespace Barotrauma var character = AIController.Character; if (character?.Removed ?? true || character.IsDead) { return; } - if (unstunY.HasValue) + if (UnstunY.HasValue) { if (PlayTimer > 4.0f) { float extent = character.AnimController.MainLimb.body.GetMaxExtent(); - if (character.SimPosition.Y < (unstunY.Value + extent * 3.0f) && + if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) && character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f) { character.IsRagdolled = false; - unstunY = null; + UnstunY = null; } else { @@ -316,7 +336,7 @@ namespace Barotrauma else { character.IsRagdolled = false; - unstunY = null; + UnstunY = null; } } @@ -362,15 +382,11 @@ namespace Barotrauma { character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime)); } - else if (Hunger < MaxHunger * 0.1f) - { - character.CharacterHealth.ReduceAllAfflictionsOnAllLimbs(8.0f * deltaTime); - } if (character.SelectedBy != null) { character.IsRagdolled = true; - unstunY = character.SimPosition.Y; + UnstunY = character.SimPosition.Y; } for (int i = 0; i < itemsToProduce.Count; i++) @@ -435,37 +451,34 @@ namespace Barotrauma spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } - var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false); - var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior; - if (petBehavior != null) + + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier()); + if (characterPrefab == null) { - petBehavior.Owner = owner; - var petBehaviorElement = subElement.Element("petbehavior"); - if (petBehaviorElement != null) + DebugConsole.ThrowError($"Failed to load the pet \"{speciesName}\". Character prefab not found."); + continue; + } + var pet = Character.Create(characterPrefab, spawnPos, seed, spawnInitialItems: false); + if (pet != null) + { + var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null) { - petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); - petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); + petBehavior.Owner = owner; + var petBehaviorElement = subElement.Element("petbehavior"); + if (petBehaviorElement != null) + { + petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); + petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); + } } } - var inventoryElement = subElement.Element("inventory"); if (inventoryElement != null) { pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null)); - } + } } } - - public void ServerWrite(IWriteMessage msg) - { - msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8); - msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8); - } - - public void ClientRead(IReadMessage msg) - { - Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8); - Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index ba29bdbb4..69165bcaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -24,7 +24,7 @@ namespace Barotrauma { if (TargetItemComponent is Turret turret) { - if (!turret.CheckTurretAngle(entity.WorldPosition)) + if (!turret.IsWithinAimingRadius(entity.WorldPosition)) { importance *= 0.1f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 2ac885b14..99d9e2fd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -350,20 +350,20 @@ namespace Barotrauma ShipIssueWorkers.Clear(); - if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent() is Reactor reactor) { var order = new Order(OrderPrefab.Prefabs["operatereactor"], "powerup".ToIdentifier(), reactor.Item, reactor); ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, order)); } - if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) + if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag(Tags.NavTerminal) && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) { steering = steeringComponent; var order = new Order(OrderPrefab.Prefabs["steer"], "navigatetactical".ToIdentifier(), nav, steeringComponent); ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); } - foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint"))) + foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag(Tags.Turret) && !i.HasTag(Tags.Hardpoint))) { var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent()); ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 34092a282..a2c281159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -80,7 +80,7 @@ namespace Barotrauma { foreach (var turret in turrets) { - turret.UpdateAutoOperate(deltaTime, friendlyTag); + turret.UpdateAutoOperate(deltaTime, ignorePower: true, friendlyTag); } } } @@ -107,7 +107,7 @@ namespace Barotrauma private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); - private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); @@ -273,6 +273,10 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); + if (!IsClient) + { + if (!initialCellsSpawned) { SpawnInitialCells(); } + } bool isSomeoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; #if SERVER @@ -322,7 +326,6 @@ namespace Barotrauma OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { - if (!initialCellsSpawned) { SpawnInitialCells(); } UpdateReinforcements(deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index 9bb81acc8..69a1f4e9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -27,7 +27,7 @@ namespace Barotrauma public string Brain { get; private set; } [Serialize("", IsPropertySaveable.No)] - public string Spawner { get; private set; } + public Identifier Spawner { get; private set; } [Serialize("", IsPropertySaveable.No)] public string BrainRoomBackground { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 1606de078..47e3ec73d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -286,17 +286,32 @@ namespace Barotrauma public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { - useItemTimer = 0.5f; + useItemTimer = 0.05f; StartUsingItem(); if (!allowMovement) { TargetMovement = Vector2.Zero; TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; - float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); - if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) + if (InWater) { - TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); + float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); + if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) + { + TargetMovement = GetTargetMovement(Vector2.Normalize(handWorldPos - character.WorldPosition)); + } + } + else + { + float distX = Math.Abs(handWorldPos.X - character.WorldPosition.X); + if (distX > ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength)) + { + TargetMovement = GetTargetMovement(Vector2.UnitX * Math.Sign(handWorldPos.X - character.WorldPosition.X)); + } + } + Vector2 GetTargetMovement(Vector2 dir) + { + return dir * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); } } @@ -308,6 +323,15 @@ namespace Barotrauma handSimPos -= character.Submarine.SimPosition; } + Vector2 refPos = rightShoulder?.WorldAnchorA ?? leftShoulder?.WorldAnchorA ?? MainLimb.SimPosition; + Vector2 diff = handSimPos - refPos; + float dist = diff.Length(); + float maxDist = ArmLength * 0.9f; + if (dist > maxDist) + { + handSimPos = refPos + diff / dist * maxDist; + } + var leftHand = GetLimb(LimbType.LeftHand); if (leftHand != null) { @@ -323,6 +347,16 @@ namespace Barotrauma rightHand.PullJointEnabled = true; rightHand.PullJointWorldAnchorB = handSimPos; } + + //make the character crouch if using an item some distance below them (= on the floor) + if (!inWater && + character.WorldPosition.Y - handWorldPos.Y > ConvertUnits.ToDisplayUnits(CurrentGroundedParams.TorsoPosition) / 4 && + this is HumanoidAnimController humanoidAnimController) + { + humanoidAnimController.Crouching = true; + humanoidAnimController.ForceSelectAnimationType = AnimationType.Crouch; + character.SetInput(InputType.Crouch, hit: false, held: true); + } } public void Grab(Vector2 rightHandPos, Vector2 leftHandPos) @@ -352,13 +386,8 @@ namespace Barotrauma //calculate the handle positions Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); - float horizontalOffset = ConvertUnits.ToSimUnits((item.Sprite.size.X / 2 - item.Sprite.Origin.X) * item.Scale); - - //handlePos[0] = ConvertUnits.ToSimUnits(new Vector2(-45,25) * 0.5f); - //handlePos[1] = ConvertUnits.ToSimUnits(new Vector2(-65,30) * 0.5f); - - transformedHandlePos[0] = Vector2.Transform(new Vector2(handlePos[0].X + horizontalOffset, handlePos[0].Y), itemTransfrom); - transformedHandlePos[1] = Vector2.Transform(new Vector2(handlePos[1].X + horizontalOffset, handlePos[1].Y), itemTransfrom); + transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom); + transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom); Limb torso = GetLimb(LimbType.Torso) ?? MainLimb; Limb leftHand = GetLimb(LimbType.LeftHand); @@ -385,7 +414,9 @@ namespace Barotrauma if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); - Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX; + Vector2 diff = holdable.Aimable ? + (mousePos - AimSourceSimPos) * Dir : + MathUtils.RotatePoint(Vector2.UnitX, torsoRotation); holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; holdAngle += GetAimWobble(rightHand, leftHand, item); itemAngle = torsoRotation + holdAngle * Dir; @@ -480,6 +511,16 @@ namespace Barotrauma return; } + float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir); + float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation); + float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25); + if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi) + { + itemRotation = targetAngle; + } + item.SetTransform(currItemPos, itemRotation, setPrevTransform: false); + previousDirection = dir; + if (holdable.Pusher != null) { if (character.Stun > 0.0f || character.IsIncapacitated) @@ -497,24 +538,11 @@ namespace Barotrauma else { holdable.Pusher.TargetPosition = currItemPos; - holdable.Pusher.TargetRotation = holdAngle * Dir; - + holdable.Pusher.TargetRotation = itemRotation; holdable.Pusher.MoveToTargetPosition(true); - - currItemPos = holdable.Pusher.SimPosition; - itemAngle = holdable.Pusher.Rotation; } } } - float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir); - float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation); - float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25); - if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi) - { - itemRotation = targetAngle; - } - item.SetTransform(currItemPos, itemRotation, setPrevTransform: false); - previousDirection = dir; if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero && (aim || !holdable.UseHandRotationForHoldAngle)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 6682b2d50..91924e625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -144,7 +144,7 @@ namespace Barotrauma set { HumanSwimFastParams = value as HumanSwimFastParams; } } - public bool Crouching; + public bool Crouching { get; set; } private float upperLegLength = 0.0f, lowerLegLength = 0.0f; @@ -197,7 +197,7 @@ namespace Barotrauma public HumanoidAnimController(Character character, string seed, HumanRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { // TODO: load from the character info file? - movementLerp = RagdollParams.MainElement.GetAttributeFloat("movementlerp", 0.4f); + movementLerp = RagdollParams?.MainElement?.GetAttributeFloat("movementlerp", 0.4f) ?? 0f; } public override void Recreate(RagdollParams ragdollParams = null) @@ -243,19 +243,14 @@ namespace Barotrauma if (MainLimb == null) { return; } levitatingCollider = !IsHanging; - ColliderIndex = Crouching && !swimming ? 1 : 0; if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || character.SelectedSecondaryItem?.GetComponent() != null || (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; - ColliderIndex = 0; - } - else if (!Crouching && ColliderIndex == 1) - { - Crouching = true; } + ColliderIndex = Crouching && !swimming ? 1 : 0; //stun (= disable the animations) if the ragdoll receives a large enough impact if (strongestImpact > 0.0f) @@ -417,6 +412,22 @@ namespace Barotrauma swimming = inWater; swimmingStateLockTimer = 0.5f; } + if (character.SelectedItem?.Prefab is { GrabWhenSelected: true } && + character.SelectedItem.ParentInventory == null && + character.SelectedItem.body is not { Enabled: true } && + character.SelectedItem.GetComponent()?.CurrentFixer != character) + { + bool moving = character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right); + moving |= (character.InWater || character.IsClimbing) && (character.IsKeyDown(InputType.Up) || character.IsKeyDown(InputType.Down)); + if (!moving) + { + Vector2 handPos = character.SelectedItem.WorldPosition - Vector2.UnitY * ConvertUnits.ToDisplayUnits(ArmLength / 2); + handPos.Y = Math.Max(handPos.Y, character.SelectedItem.WorldRect.Y - character.SelectedItem.WorldRect.Height); + UpdateUseItem( + allowMovement: false, + handPos); + } + } if (swimming) { UpdateSwimming(); @@ -616,7 +627,7 @@ namespace Barotrauma if (TorsoAngle.HasValue && !torso.Disabled) { float torsoAngle = TorsoAngle.Value; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); + float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index cc28d59c1..c9ac1f88b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -744,6 +744,13 @@ namespace Barotrauma { if (character.DisableImpactDamageTimer > 0.0f) { return; } + if (f2.Body?.UserData is Item) + { + //no impact damage from items + //items that can impact characters (melee weapons, projectiles) should handle the damage themselves + return; + } + Vector2 normal = localNormal; float impact = Vector2.Dot(velocity, -normal); if (f1.Body == Collider.FarseerBody || !Collider.Enabled) @@ -753,16 +760,18 @@ namespace Barotrauma if (isNotRemote) { - if (impact > ImpactTolerance) + float impactTolerance = ImpactTolerance; + if (character.Stun > 0.0f) { impactTolerance *= 0.5f; } + if (impact > impactTolerance) { impactPos = ConvertUnits.ToDisplayUnits(impactPos); - if (character.Submarine != null) impactPos += character.Submarine.Position; + if (character.Submarine != null) { impactPos += character.Submarine.Position; } - float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage); + float impactDamage = GetImpactDamage(impact, impactTolerance); character.LastDamageSource = null; character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true); - strongestImpact = Math.Max(strongestImpact, impact - ImpactTolerance); + strongestImpact = Math.Max(strongestImpact, impact - impactTolerance); character.ApplyStatusEffects(ActionType.OnImpact, 1.0f); //briefly disable impact damage //otherwise the character will take damage multiple times when for example falling, @@ -776,6 +785,12 @@ namespace Barotrauma ImpactProjSpecific(impact, f1.Body); } + public float GetImpactDamage(float impact, float? impactTolerance = null) + { + float tolerance = impactTolerance ?? ImpactTolerance; + return Math.Min((impact - tolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage); + } + private readonly List connectedLimbs = new List(); private readonly List checkedJoints = new List(); public bool SeverLimbJoint(LimbJoint limbJoint) @@ -1023,7 +1038,12 @@ namespace Barotrauma CurrentHull = newHull; character.Submarine = currentHull?.Submarine; - character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform()); + foreach (var attachedProjectile in character.AttachedProjectiles) + { + attachedProjectile.Item.CurrentHull = currentHull; + attachedProjectile.Item.Submarine = character.Submarine; + attachedProjectile.Item.UpdateTransform(); + } } private void PreventOutsideCollision() @@ -1323,6 +1343,11 @@ namespace Barotrauma if (Collider.LinearVelocity == Vector2.Zero) { character.IsRagdolled = true; + if (character.IsBot) + { + // Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots. + character.SetInput(InputType.Ragdoll, hit: false, held: true); + } } } } @@ -1781,12 +1806,6 @@ namespace Barotrauma Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); Character.Latchers.Clear(); - if (detachProjectiles) - { - character.AttachedProjectiles.ForEachMod(p => p?.Unstick()); - character.AttachedProjectiles.Clear(); - } - Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { @@ -1823,7 +1842,7 @@ namespace Barotrauma protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; - + Vector2 prevPosition = limb.body.SimPosition; if (Vector2.DistanceSquared(original, simPosition) > 0.0001f) { Category collisionCategory = Physics.CollisionWall | Physics.CollisionLevel; @@ -1851,6 +1870,16 @@ namespace Barotrauma limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA; limb.PullJointEnabled = false; } + foreach (var attachedProjectile in character.AttachedProjectiles) + { + if (attachedProjectile.IsAttachedTo(limb.body)) + { + attachedProjectile.Item.SetTransform( + attachedProjectile.Item.SimPosition + (movePos - prevPosition), + attachedProjectile.Item.body.Rotation, + findNewHull: false); + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 5991498e5..f3397c089 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -210,8 +210,8 @@ namespace Barotrauma [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] public float SwayFrequency { get; set; } - [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")] - public float Stun { get; private set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy functionality. Behaves otherwise the same as stuns defined as afflictions, but explosions only apply the stun once instead of dividing it between the limbs.")] + public float Stun { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; set; } @@ -434,13 +434,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - Conditionals.Add(new PropertyConditional(attribute)); - } - } + Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } @@ -544,8 +538,7 @@ namespace Barotrauma } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - // TODO: do we need the conversion to list here? It generates garbage. - var targets = targetCharacter.AnimController.Limbs.Cast().ToList(); + var targets = targetCharacter.AnimController.Limbs; if (additionalEffectType != ActionType.OnEating) { effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets); @@ -612,7 +605,10 @@ namespace Barotrauma float penetration = Penetration; - float? penetrationValue = SourceItem?.GetComponent()?.Penetration; + RangedWeapon weapon = + SourceItem?.GetComponent() ?? + SourceItem?.GetComponent()?.Launcher?.GetComponent(); + float? penetrationValue = weapon?.Penetration; if (penetrationValue.HasValue) { penetration += penetrationValue.Value; @@ -646,8 +642,7 @@ namespace Barotrauma } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - // TODO: do we need the conversion to list here? It generates garbage. - var targets = targetLimb.character.AnimController.Limbs.Cast().ToList(); + var targets = targetLimb.character.AnimController.Limbs; effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets); effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 6042aecee..41847f24f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -65,6 +65,13 @@ namespace Barotrauma } UpdateLimbLightSource(limb); } + foreach (var item in HeldItems) + { + if (item.body != null) + { + item.body.Enabled = enabled; + } + } AnimController.Collider.Enabled = value; } } @@ -335,7 +342,7 @@ namespace Barotrauma public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; public CombatAction CombatAction; - public AnimController AnimController; + public readonly AnimController AnimController; private Vector2 cursorPosition; @@ -386,6 +393,8 @@ namespace Barotrauma public bool IsMachine => Params.IsMachine; public bool IsHusk => Params.Husk; + public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType("disguiseashusk".ToIdentifier()) > 0; + public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains("huskinfected".ToIdentifier()); public bool IsMale => info?.IsMale ?? false; @@ -781,7 +790,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.IsParalyzed; } } @@ -792,7 +801,7 @@ namespace Barotrauma public bool IsArrested { - get { return IsHuman && HasEquippedItem("handlocker"); } + get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); } } public bool IsPet @@ -852,6 +861,7 @@ namespace Barotrauma public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float EmpVulnerability => Params.Health.EmpVulnerability; public float PoisonVulnerability => Params.Health.PoisonVulnerability; + public bool IsFlipped => AnimController.IsFlipped; public float Bloodloss { @@ -865,7 +875,7 @@ namespace Barotrauma public float Bleeding { - get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); } + get { return CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.BleedingType, true); } } private bool speechImpedimentSet; @@ -936,6 +946,8 @@ namespace Barotrauma { GameMain.GameSession?.CrewManager?.AutoHideCrewList(); } + + _selectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); } #endif if (prevSelectedItem != null && (_selectedItem == null || _selectedItem != prevSelectedItem) && itemSelectedTime > 0) @@ -1073,8 +1085,17 @@ namespace Barotrauma public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100; + /// + /// Godmoded characters cannot receive any afflictions whatsoever + /// public bool GodMode = false; + public bool Unkillable + { + get { return CharacterHealth.Unkillable; } + set { CharacterHealth.Unkillable = value; } + } + public CampaignMode.InteractionType CampaignInteractionType; public Identifier MerchantIdentifier; @@ -1308,9 +1329,9 @@ namespace Barotrauma break; } } - if (Params.VariantFile != null) + if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement) { - var overrideElement = Params.VariantFile.Root.FromPackage(Params.MainElement.ContentPackage); + var overrideElement = Params.VariantFile.Root.FromPackage(paramsMainElement.ContentPackage); // Only override if the override file contains matching elements if (overrideElement.GetChildElement("inventory") != null) { @@ -1484,7 +1505,7 @@ namespace Barotrauma var head = AnimController.GetLimb(LimbType.Head); if (head == null) { return; } // Note that if there are any other wearables on the head, they are removed here. - head.OtherWearables.ForEach(w => w.Sprite.Remove()); + head.OtherWearables.ForEach(w => w.Sprite?.Remove()); head.OtherWearables.Clear(); //if the element has not been set at this point, the character has no hair and the index should be zero (= no hair) @@ -1664,17 +1685,31 @@ namespace Barotrauma info.Job?.GiveJobItems(this, spawnPoint); } - public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) + + public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) { - if (info?.Job == null || spawnPoint == null) { return; } + GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent); + } + + public void GiveIdCardTags(IEnumerable spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) + { + if (info?.Job == null || spawnPoints == null) { return; } foreach (Item item in Inventory.AllItems) { - if (item?.GetComponent() == null) { continue; } - foreach (string s in spawnPoint.IdCardTags) + if (item?.GetComponent() is not IdCard idCard) { continue; } + if (requireSpawnPointTagsNotGiven) { - item.AddTag(s); + if (idCard.SpawnPointTagsGiven) { continue; } } + 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)); @@ -1983,14 +2018,21 @@ namespace Barotrauma if (AnimController is HumanoidAnimController humanAnimController) { - humanAnimController.Crouching = humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || IsKeyDown(InputType.Crouch); + humanAnimController.Crouching = + humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || + IsKeyDown(InputType.Crouch); + if (Screen.Selected is not { IsEditor: true }) + { + humanAnimController.ForceSelectAnimationType = AnimationType.NotDefined; + } } if (!aiControlled && !AnimController.IsUsingItem && AnimController.Anim != AnimController.Animation.CPR && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this) && - (AnimController.OnGround || IsClimbing) && !AnimController.InWater) + ((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) && + !AnimController.InWater) { if (dontFollowCursor) { @@ -2187,14 +2229,14 @@ namespace Barotrauma { if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) { - item.Use(deltaTime, this); + item.Use(deltaTime, user: this); } } if (IsKeyDown(InputType.Shoot) && item.IsShootable) { if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) { - item.Use(deltaTime, this); + item.Use(deltaTime, user: this); } #if CLIENT else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) @@ -2214,7 +2256,7 @@ namespace Barotrauma { if (!SelectedCharacter.CanBeSelected || (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance && - SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance))) + SelectedCharacter.GetDistanceToClosestLimb(GetRelativeSimPosition(selectedCharacter, WorldPosition)) > ConvertUnits.ToSimUnits(MaxDragDistance))) { DeselectCharacter(); } @@ -2247,26 +2289,58 @@ namespace Barotrauma }; } - public bool CanSeeCharacter(Character target) + private Limb GetSeeingLimb() + { + return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; + } + + public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false) + { + seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb(); + if (target is Character targetCharacter) + { + return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + } + else + { + return CheckVisibility(target, seeingEntity, checkFacing); + } + } + + public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) + { + if (seeingEntity is Character seeingCharacter) + { + return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing); + } + if (target is Character targetCharacter) + { + return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + } + else + { + return CheckVisibility(target, seeingEntity, checkFacing); + } + } + + private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); - if (target == null) { return false; } - if (target.Removed) { return false; } - Limb seeingLimb = GetSeeingLimb(); - if (CanSeeTarget(target, seeingLimb)) { return true; } + if (target == null || target.Removed) { return false; } + if (CheckVisibility(target, seeingEntity, 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) Limb leftExtremity = null, rightExtremity = null; float leftMostDot = 0.0f, rightMostDot = 0.0f; - Vector2 dir = target.WorldPosition - WorldPosition; + Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition; Vector2 leftDir = new Vector2(dir.Y, -dir.X); Vector2 rightDir = new Vector2(-dir.Y, dir.X); foreach (Limb limb in target.AnimController.Limbs) { if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; } if (limb.Hidden) { continue; } - Vector2 limbDir = limb.WorldPosition - WorldPosition; + Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition; float leftDot = Vector2.Dot(limbDir, leftDir); if (leftDot > leftMostDot) { @@ -2282,42 +2356,39 @@ namespace Barotrauma continue; } } - if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; } - if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; } + if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; } + if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; } } return false; } - private Limb GetSeeingLimb() - { - return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; - } - - public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null) + private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); if (target == null) { return false; } - seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity; if (seeingEntity == null) { return false; } - ISpatialEntity sourceEntity = seeingEntity ; // TODO: Could we just use the method below? If not, let's refactor it so that we can. - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceEntity.WorldPosition); + Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); + if (checkFacing && seeingEntity is Character seeingCharacter) + { + 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 == Submarine || target.Submarine == null) + if (target.Submarine == seeingEntity.Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); } //we're outside, the other character inside - else if (Submarine == null) + else if (seeingEntity.Submarine == null) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); } //both inside different subs else { - closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); if (!IsBlocking(closestBody)) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); @@ -2370,9 +2441,6 @@ namespace Barotrauma return false; } - public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) - => HasEquippedItem(tagOrIdentifier.ToIdentifier(), allowBroken, slotType); - public bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) { if (Inventory == null) { return false; } @@ -2394,7 +2462,7 @@ namespace Barotrauma return false; } - public Item GetEquippedItem(string tagOrIdentifier = null, InvSlotType? slotType = null) + public Item GetEquippedItem(Identifier? tagOrIdentifier = null, InvSlotType? slotType = null) { if (Inventory == null) { return null; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2409,7 +2477,10 @@ namespace Barotrauma } var item = Inventory.GetItemAt(i); if (item == null) { continue; } - if (tagOrIdentifier == null || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } + if (tagOrIdentifier == null || tagOrIdentifier.Value.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier.Value)) + { + return item; + } } return null; } @@ -2537,7 +2608,7 @@ namespace Barotrauma } } - return !checkVisibility || CanSeeCharacter(c); + return !checkVisibility || CanSeeTarget(c); } public bool CanInteractWith(Item item, bool checkLinked = true) @@ -2587,7 +2658,10 @@ namespace Barotrauma { foreach (MapEntity linked in item.linkedTo) { - if (linked is Item linkedItem) + if (linked is Item linkedItem && + //if the linked item is inside this container (a modder or sub builder doing smth really weird?) + //don't check it here because it'd lead to an infinite loop + linkedItem.ParentInventory?.Owner != item) { if (CanInteractWith(linkedItem, out float distToLinked, checkLinked: false)) { @@ -2675,10 +2749,18 @@ namespace Barotrauma if (SelectedSecondaryItem != null && !item.IsSecondaryItem) { + //don't allow selecting another Controller if it'd try to turn the character in the opposite direction + //(e.g. periscope that's facing the wrong way while sitting in a chair) if (item.GetComponent() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; } - float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin); - if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; } - if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; } + + //if a Controller that controls the character's pose is selected, + //don't allow selecting items that are behind the character's back + if (SelectedSecondaryItem.GetComponent() is { ControlCharacterPose: true } selectedController) + { + float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin); + if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; } + if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; } + } } if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) @@ -2703,7 +2785,7 @@ namespace Barotrauma this.onCustomInteract = onCustomInteract; CustomInteractHUDText = hudText; } - + public void SelectCharacter(Character character) { if (character == null || character == this) { return; } @@ -2754,7 +2836,7 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld) { FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; - if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } + if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { @@ -2867,7 +2949,7 @@ namespace Barotrauma } #endif } - else + else if (!IsClimbing) { #if CLIENT if (Controlled == this) @@ -2915,9 +2997,9 @@ namespace Barotrauma CharacterHealth.OpenHealthWindow = null; #endif } - else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null)) + else if (IsKeyHit(InputType.Health) && SelectedItem != null) { - SelectedItem = SelectedSecondaryItem = null; + SelectedItem = null; } else if (focusedItem != null) { @@ -3107,6 +3189,14 @@ namespace Barotrauma //implode if not protected from pressure, and either outside or in a high-pressure hull if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { + if (PressureTimer > CharacterHealth.PressureKillDelay * 0.1f) + { + //after a brief delay, start doing increasing amounts of organ damage + CharacterHealth.ApplyAffliction( + targetLimb: AnimController.MainLimb, + new Affliction(AfflictionPrefab.OrganDamage, PressureTimer / 10.0f * deltaTime)); + } + if (CharacterHealth.PressureKillDelay <= 0.0f) { PressureTimer = 100.0f; @@ -3176,47 +3266,48 @@ namespace Barotrauma UpdateAIChatMessages(deltaTime); - if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true) + bool wasRagdolled = IsRagdolled; + if (IsForceRagdolled) { - bool wasRagdolled = IsRagdolled; - if (IsForceRagdolled) + IsRagdolled = IsForceRagdolled; + } + else if (this != Controlled) + { + wasRagdolled = IsRagdolled; + IsRagdolled = IsKeyDown(InputType.Ragdoll); + if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true }) { - IsRagdolled = IsForceRagdolled; - } - else if (this != Controlled) - { - wasRagdolled = IsRagdolled; - IsRagdolled = IsKeyDown(InputType.Ragdoll); - } - else - { - bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); - bool bodyMovingTooFast(PhysicsBody body) - { - return - body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || - //falling down counts as going too fast - (!InWater && body.LinearVelocity.Y < -5.0f); - } - if (ragdollingLockTimer > 0.0f) - { - ragdollingLockTimer -= deltaTime; - } - else if (!tooFastToUnragdoll) - { - IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } - } - if (IsRagdolled) - { - SetInput(InputType.Ragdoll, false, true); - } - } - if (!wasRagdolled && IsRagdolled) - { - CheckTalents(AbilityEffectType.OnRagdoll); + ClearInput(InputType.Ragdoll); } } + else + { + bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); + bool bodyMovingTooFast(PhysicsBody body) + { + return + body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || + //falling down counts as going too fast + (!InWater && body.LinearVelocity.Y < -5.0f); + } + if (ragdollingLockTimer > 0.0f) + { + ragdollingLockTimer -= deltaTime; + } + else if (!tooFastToUnragdoll) + { + IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + } + if (IsRagdolled) + { + SetInput(InputType.Ragdoll, false, true); + } + } + if (!wasRagdolled && IsRagdolled) + { + CheckTalents(AbilityEffectType.OnRagdoll); + } lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); @@ -3419,7 +3510,7 @@ namespace Barotrauma } private float despawnTimer; - private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true) + private void UpdateDespawn(float deltaTime, bool createNetworkEvents = true) { if (!EnableDespawn) { return; } @@ -3430,7 +3521,7 @@ namespace Barotrauma int subCorpseCount = 0; - if (Submarine != null && !ignoreThresholds) + if (Submarine != null) { subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; } @@ -3464,6 +3555,11 @@ namespace Barotrauma despawnTimer += deltaTime * despawnPriority; if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } + Despawn(); + } + + private void Despawn(bool createNetworkEvents = true) + { Identifier despawnContainerId = IsHuman ? "despawncontainer".ToIdentifier() : @@ -3514,8 +3610,7 @@ namespace Barotrauma public void DespawnNow(bool createNetworkEvents = true) { - despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; - UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); + Despawn(createNetworkEvents); //update twice: first to spawn the duffel bag and move the items into it, then to remove the character for (int i = 0; i < 2; i++) { @@ -4259,7 +4354,7 @@ namespace Barotrauma if (Screen.Selected != GameMain.GameScreen) { return; } if (newStun > 0 && Params.Health.StunImmunity) { - if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -4323,8 +4418,7 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (limb.type == limbType) { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, this, limb); } } } @@ -4334,8 +4428,7 @@ namespace Barotrauma Limb limb = AnimController.GetLimb(limbType); if (limb != null) { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, this, limb); } } else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) @@ -4344,12 +4437,20 @@ namespace Barotrauma Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); if (limb != null) { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, this, limb); } } } } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all limbs + foreach (var limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + ApplyToLimb(actionType, deltaTime, statusEffect, character: this, limb); + } + } if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffect.Apply(actionType, deltaTime, this, this); @@ -4373,6 +4474,12 @@ namespace Barotrauma { CharacterHealth.ApplyAfflictionStatusEffects(actionType); } + + static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb) + { + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, entity: character, target: limb); + } } private void Implode(bool isNetworkMessage = false) @@ -4433,7 +4540,7 @@ namespace Barotrauma public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { - if (IsDead || CharacterHealth.Unkillable || GodMode) { return; } + if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; } HealthUpdateInterval = 0.0f; @@ -4533,20 +4640,21 @@ namespace Barotrauma SelectedCharacter = null; AnimController.ResetPullJoints(); - - foreach (var joint in AnimController.LimbJoints) + if (AnimController.LimbJoints != null) { - if (joint.revoluteJoint != null) + foreach (var joint in AnimController.LimbJoints) { - joint.revoluteJoint.MotorEnabled = false; + if (joint.revoluteJoint != null) + { + joint.revoluteJoint.MotorEnabled = false; + } } } - GameMain.GameSession?.KillCharacter(this); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); - public void Revive(bool removeAllAfflictions = true) + public void Revive(bool removeAfflictions = true) { if (Removed) { @@ -4557,20 +4665,21 @@ namespace Barotrauma aiTarget?.Remove(); aiTarget = new AITarget(this); - if (removeAllAfflictions) + if (removeAfflictions) { CharacterHealth.RemoveAllAfflictions(); + SetAllDamage(0.0f, 0.0f, 0.0f); + Bloodloss = 0.0f; + SetStun(0.0f, true); } - else - { - CharacterHealth.RemoveNegativeAfflictions(); - } - SetAllDamage(0.0f, 0.0f, 0.0f); Oxygen = 100.0f; - Bloodloss = 0.0f; - SetStun(0.0f, true); isDead = false; + if (info != null) + { + info.CauseOfDeath = null; + } + foreach (LimbJoint joint in AnimController.LimbJoints) { var revoluteJoint = joint.revoluteJoint; @@ -4594,10 +4703,7 @@ namespace Barotrauma limb.IsSevered = false; } - if (GameMain.GameSession != null) - { - GameMain.GameSession.ReviveCharacter(this); - } + GameMain.GameSession?.ReviveCharacter(this); } public override void Remove() @@ -4712,7 +4818,7 @@ namespace Barotrauma { SpawnInventoryItemsRecursive(inventory, itemData, new List()); } - + private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List extraDuffelBags) { foreach (var itemElement in element.Elements()) @@ -4737,21 +4843,6 @@ namespace Barotrauma continue; } - //make sure there's no other item in the slot - //this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign - for (int i = 0; i < inventory.Capacity; i++) - { - if (slotIndices.Contains(i)) - { - var existingItem = inventory.GetItemAt(i); - if (existingItem != null && existingItem != newItem && (((MapEntity)existingItem).Prefab != ((MapEntity)newItem).Prefab || existingItem.Prefab.MaxStackSize == 1)) - { - DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\""); - existingItem.Drop(null, createNetworkEvent: false); - } - } - } - bool canBePutInOriginalInventory = true; if (slotIndices[0] >= inventory.Capacity) { @@ -4833,6 +4924,13 @@ namespace Barotrauma } } +#if SERVER + foreach (var circuitBox in newItem.GetComponents()) + { + circuitBox.MarkServerRequiredInitialization(); + } +#endif + int itemContainerIndex = 0; var itemContainers = newItem.GetComponents().ToList(); foreach (var childInvElement in itemElement.Elements()) @@ -4921,41 +5019,7 @@ namespace Barotrauma return visibleHulls; } - public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos); - - public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null) - { - Vector2 targetPos = to.SimPosition; - if (worldPos.HasValue) - { - Vector2 wp = worldPos.Value; - if (to.Submarine != null) - { - wp -= to.Submarine.Position; - } - targetPos = ConvertUnits.ToSimUnits(wp); - } - if (from.Submarine == null && to.Submarine != null) - { - // outside and targeting inside - targetPos += to.Submarine.SimPosition; - } - else if (from.Submarine != null && to.Submarine == null) - { - // inside and targeting outside - targetPos -= from.Submarine.SimPosition; - } - else if (from.Submarine != to.Submarine) - { - if (from.Submarine != null && to.Submarine != null) - { - // both inside, but in different subs - Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition; - targetPos -= diff; - } - } - return targetPos; - } + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => Submarine.GetRelativeSimPosition(this, target, worldPos); public bool IsCaptain => HasJob("captain"); public bool IsEngineer => HasJob("engineer"); @@ -4966,6 +5030,8 @@ namespace Barotrauma public bool IsWatchman => HasJob("watchman"); public bool IsVip => HasJob("prisoner"); public bool IsPrisoner => HasJob("prisoner"); + public bool IsKiller => HasJob("killer"); + public Color? UniqueNameColor { get; set; } = null; public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; @@ -5052,6 +5118,7 @@ namespace Barotrauma public bool HasTalent(Identifier identifier) { + if (info == null) { return false; } return info.UnlockedTalents.Contains(identifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 9786888d3..954809cb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -37,12 +37,18 @@ namespace Barotrauma public EventType EventType { get; } } - public struct InventoryStateEventData : IEventData + public readonly struct InventoryStateEventData : IEventData { public EventType EventType => EventType.InventoryState; + public readonly Range SlotRange; + + public InventoryStateEventData(Range slotRange) + { + SlotRange = slotRange; + } } - public struct ControlEventData : IEventData + public readonly struct ControlEventData : IEventData { public EventType EventType => EventType.Control; public readonly Client Owner; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index fde9954bd..61f4df9c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -440,10 +440,9 @@ namespace Barotrauma { if (handleBuff) { - var head = Character.AnimController.GetLimb(LimbType.Head); - if (head != null) + if (AfflictionPrefab.Prefabs.TryGet("disguised", out AfflictionPrefab afflictionPrefab)) { - Character.CharacterHealth.ApplyAffliction(head, AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); + Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(100f)); } } @@ -464,11 +463,7 @@ namespace Barotrauma if (handleBuff) { - var head = Character.AnimController.GetLimb(LimbType.Head); - if (head != null) - { - Character.CharacterHealth.ReduceAfflictionOnLimb(head, "disguised".ToIdentifier(), 100f); - } + Character.CharacterHealth.ReduceAfflictionOnAllLimbs("disguised".ToIdentifier(), 100f); } } @@ -683,11 +678,15 @@ namespace Barotrauma FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - var headPreset = Prefab.Heads.GetRandom(randSync); + var headPreset = Prefab?.Heads.GetRandom(randSync); + if (headPreset == null) + { + DebugConsole.ThrowError("Failed to find a head preset!"); + } Head = new HeadInfo(this, headPreset); SetAttachments(randSync); SetColors(randSync); - + Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant)); if (!string.IsNullOrEmpty(name)) @@ -1089,6 +1088,7 @@ namespace Barotrauma private void LoadHeadSprite() { + if (Ragdoll?.MainElement == null) { return; } foreach (var limbElement in Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -1386,7 +1386,7 @@ namespace Barotrauma // Replace the name tag of any existing id cards or duffel bags foreach (var item in Item.ItemList) { - if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; } + if (!item.HasTag("identitycard".ToIdentifier()) && !item.HasTag("despawncontainer".ToIdentifier())) { continue; } foreach (var tag in item.Tags.Split(',')) { var splitTag = tag.Split(":"); @@ -1446,7 +1446,7 @@ namespace Barotrauma if (MinReputationToHire.factionId != default) { charElement.Add( - new XAttribute("factionId", Name), + new XAttribute("factionId", MinReputationToHire.factionId), new XAttribute("minreputation", MinReputationToHire.reputation)); } @@ -1655,8 +1655,12 @@ namespace Barotrauma continue; } var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0); - int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1); - var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null; + Character orderGiver = null; + if (orderElement.GetAttribute("ordergiver") is XAttribute orderGiverIdAttribute) + { + int orderGiverInfoId = orderGiverIdAttribute.GetAttributeInt(0); + orderGiver = Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId); + } Entity targetEntity = null; switch (targetType) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index d38730699..2dfb6ecf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -46,6 +46,8 @@ namespace Barotrauma public static IEnumerable ConfigElements => Prefabs.Select(p => p.ConfigElement); public static readonly Identifier HumanSpeciesName = "human".ToIdentifier(); + public static readonly Identifier HumanGroup = "human".ToIdentifier(); + public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 3b65cc320..5d46b8207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using System.Collections.Immutable; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma { @@ -602,7 +603,6 @@ namespace Barotrauma public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); - public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier(); public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; @@ -612,6 +612,7 @@ namespace Barotrauma public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; public static AfflictionPrefab Pressure => Prefabs["pressure"]; + public static AfflictionPrefab OrganDamage => Prefabs["organdamage"]; public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; @@ -841,20 +842,16 @@ namespace Barotrauma /// public readonly Sprite AfflictionOverlay; - public IEnumerable> TreatmentSuitability + public ImmutableDictionary TreatmentSuitabilities { - get - { - foreach (var itemPrefab in ItemPrefab.Prefabs) - { - float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); - if (!MathUtils.NearlyEqual(suitability, 0.0f)) - { - yield return new KeyValuePair(itemPrefab.Identifier, suitability); - } - } - } - } + get; + private set; + } = new Dictionary().ToImmutableDictionary(); + + /// + /// Can this affliction be treated with some item? + /// + public bool HasTreatments { get; private set; } public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", "")) { @@ -974,6 +971,22 @@ namespace Barotrauma constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + private void RefreshTreatmentSuitabilities() + { + var newTreatmentSuitabilities = new Dictionary(); + + foreach (var itemPrefab in ItemPrefab.Prefabs) + { + float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); + if (!MathUtils.NearlyEqual(suitability, 0.0f)) + { + newTreatmentSuitabilities.TryAdd(itemPrefab.Identifier, suitability); + } + } + HasTreatments = newTreatmentSuitabilities.Any(kvp => kvp.Value > 0); + TreatmentSuitabilities = newTreatmentSuitabilities.ToImmutableDictionary(); + } + public LocalizedString GetDescription(float strength, Description.TargetType targetType) { foreach (var description in Descriptions) @@ -993,9 +1006,16 @@ namespace Barotrauma return defaultDescription; } - public static void LoadAllEffects() + /// + /// Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities. + /// + public static void LoadAllEffectsAndTreatmentSuitabilities() { - Prefabs.ForEach(p => p.LoadEffects()); + foreach (var prefab in Prefabs) + { + prefab.RefreshTreatmentSuitabilities(); + prefab.LoadEffects(); + } } public static void ClearAllEffects() @@ -1003,7 +1023,7 @@ namespace Barotrauma Prefabs.ForEach(p => p.ClearEffects()); } - public void LoadEffects() + private void LoadEffects() { ClearEffects(); foreach (var subElement in configElement.Elements()) @@ -1032,7 +1052,7 @@ namespace Barotrauma } } - public void ClearEffects() + private void ClearEffects() { effects.Clear(); periodicEffects.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 95dad7ad3..d7567b91c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -156,6 +156,12 @@ namespace Barotrauma } } + /// + /// How much vitality the character would have if it was alive? + /// E.g. a character killed by disconnection or with console commands may not have any vitality-reducing afflictions despite being dead + /// + public float VitalityDisregardingDeath => vitality; + public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality); public float MaxVitality @@ -229,6 +235,8 @@ namespace Barotrauma } } + public bool IsParalyzed { get; private set; } + public float StunTimer { get; private set; } /// @@ -402,7 +410,17 @@ namespace Barotrauma return strength; } - public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true) + public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true) + { + return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions); + } + + public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true) + { + return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions); + } + + public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true) { float strength = 0.0f; foreach (KeyValuePair kvp in afflictions) @@ -410,7 +428,8 @@ namespace Barotrauma if (!allowLimbAfflictions && kvp.Value != null) { continue; } var affliction = kvp.Key; if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } - if (affliction.Prefab.AfflictionType == afflictionType) + if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) && + (affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty)) { strength += affliction.Strength; } @@ -714,7 +733,7 @@ namespace Barotrauma if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) { - if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) + if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -953,6 +972,7 @@ namespace Barotrauma public void CalculateVitality() { Vitality = MaxVitality; + IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } foreach (KeyValuePair kvp in afflictions) @@ -966,6 +986,12 @@ namespace Barotrauma } Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); + + if (affliction.Strength >= affliction.Prefab.MaxStrength && + affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + IsParalyzed = true; + } } #if CLIENT if (IsUnconscious) @@ -1136,7 +1162,7 @@ namespace Barotrauma } } - foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitabilities) { float suitability = treatment.Value * strength; if (treatment.Value > strength) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index e1748b990..887743c78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -134,7 +134,13 @@ namespace Barotrauma foreach (XElement itemElement in spawnItems.GetChildElements("Item")) { InitializeJobItem(character, itemElement, spawnPoint); - } + } + + if (GameMain.GameSession is { TraitorsEnabled: true } && character.IsSecurity) + { + var traitorGuidelineItem = ItemPrefab.Prefabs.Find(ip => ip.Tags.Contains(Tags.TraitorGuidelinesForSecurity)); + Entity.Spawner.AddItemToSpawnQueue(traitorGuidelineItem, character.Inventory); + } } private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null) @@ -144,7 +150,7 @@ namespace Barotrauma { string itemName = itemElement.Attribute("name").Value; DebugConsole.ThrowError("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items."); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab; if (itemPrefab == null) { DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 5cea1b66f..52d67296f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -591,7 +591,10 @@ namespace Barotrauma public bool IsDead => character.IsDead; public float Health => character.Health; public float HealthPercentage => character.HealthPercentage; + public bool IsHuman => character.IsHuman; + public AIState AIState => character.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; + public bool IsFlipped => character.AnimController.IsFlipped; public bool CanBeSeveredAlive { @@ -881,7 +884,9 @@ namespace Barotrauma public void Update(float deltaTime) { UpdateProjSpecific(deltaTime); - + ApplyStatusEffects(ActionType.Always, deltaTime); + ApplyStatusEffects(ActionType.OnActive, deltaTime); + if (InWater) { body.ApplyWaterForces(); @@ -1223,6 +1228,7 @@ namespace Barotrauma if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } foreach (StatusEffect statusEffect in statusEffectList) { + statusEffect.sourceBody = body; if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } @@ -1241,65 +1247,64 @@ namespace Barotrauma statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, character, targets); } - else + else if (statusEffect.targetLimbs != null) { - - if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory) + foreach (var limbType in statusEffect.targetLimbs) { - foreach (Item item in inventory.AllItems) + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - if (statusEffect.TargetIdentifiers != null && - !statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) && - statusEffect.TargetIdentifiers.None(id => item.HasTag(id))) + // Target all matching limbs + foreach (var limb in ragdoll.Limbs) { - continue; - } - if (statusEffect.TargetSlot > -1) - { - if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; } - } - targets.Add(item); - } - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) - { - statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); - } - else if (statusEffect.targetLimbs != null) - { - foreach (var limbType in statusEffect.targetLimbs) - { - if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) - { - // Target all matching limbs - foreach (var limb in ragdoll.Limbs) + if (limb.IsSevered) { continue; } + if (limb.type == limbType) { - if (limb.IsSevered) { continue; } - if (limb.type == limbType) - { - statusEffect.Apply(actionType, deltaTime, character, limb); - } + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) || statusEffect.HasTargetType(StatusEffect.TargetType.Character) || statusEffect.HasTargetType(StatusEffect.TargetType.This)) + { + // Target just the first matching limb + Limb limb = ragdoll.GetLimb(limbType); + if (limb != null) { - // Target just the first matching limb - Limb limb = ragdoll.GetLimb(limbType); - statusEffect.Apply(actionType, deltaTime, character, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + if (limb != null) { - // Target just the last matching limb - Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); - statusEffect.Apply(actionType, deltaTime, character, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } } } - else + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all limbs + foreach (var limb in ragdoll.Limbs) { - statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + if (limb.IsSevered) { continue; } + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb: this); + } + } + static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb) + { + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, entity: character, target: limb); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 9289a90d5..0fedaedf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -143,7 +143,14 @@ namespace Barotrauma protected override string GetName() => "Character Config File"; - public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; + public override ContentXElement MainElement + { + get + { + if (base.MainElement == null) { return null; } + return base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; + } + } public static XElement CreateVariantXml(XElement variantXML, XElement baseXML) { @@ -182,6 +189,11 @@ namespace Barotrauma { UpdatePath(File.Path); doc = XMLExtensions.TryLoadXml(Path); + if (MainElement == null) + { + DebugConsole.ThrowError("Main element null! Failed to load character params."); + return false; + } Identifier variantOf = MainElement.VariantOf(); if (!variantOf.IsEmpty) { @@ -230,6 +242,11 @@ namespace Barotrauma protected void CreateSubParams() { + if (MainElement == null) + { + DebugConsole.ThrowError("Main element null, cannot create sub params!"); + return; + } SubParams.Clear(); var healthElement = MainElement.GetChildElement("health"); if (healthElement != null) @@ -745,7 +762,7 @@ namespace Barotrauma { if (!TryGetTarget(targetCharacter.SpeciesName, out target)) { - target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetCharacter.Params.Group.ToString(), StringComparison.OrdinalIgnoreCase)); + target = targets.FirstOrDefault(t => t.Tag == targetCharacter.Params.Group); } return target != null; } @@ -791,7 +808,7 @@ namespace Barotrauma public override string Name => "Target"; [Serialize("", IsPropertySaveable.Yes, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()] - public string Tag { get; private set; } + public Identifier Tag { get; private set; } [Serialize(AIState.Idle, IsPropertySaveable.Yes), Editable] public AIState State { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 76e138579..03f56b285 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -71,7 +71,7 @@ namespace Barotrauma element ??= MainElement; if (element == null) { - DebugConsole.ThrowError("[EditableParams] The XML element is null!"); + DebugConsole.ThrowError("[EditableParams] The XML element is null! Failed to save the parameters."); return false; } SerializableProperty.SerializeProperties(this, element, true); @@ -82,7 +82,16 @@ namespace Barotrauma { UpdatePath(file); doc = XMLExtensions.TryLoadXml(Path); - if (doc == null) { return false; } + if (doc == null) + { + DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters."); + return false; + } + if (MainElement == null) + { + DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters."); + return false; + } IsLoaded = Deserialize(MainElement); OriginalElement = new XElement(MainElement).FromPackage(MainElement.ContentPackage); return IsLoaded; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index be86bd432..9187430ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -315,20 +315,26 @@ namespace Barotrauma protected void CreateColliders() { Colliders.Clear(); - for (int i = 0; i < MainElement.GetChildElements("collider").Count(); i++) + if (MainElement?.GetChildElements("collider") is { } colliderElements) { - var element = MainElement.GetChildElements("collider").ElementAt(i); - string name = i > 0 ? "Secondary Collider" : "Main Collider"; - Colliders.Add(new ColliderParams(element, this, name)); + for (int i = 0; i < colliderElements.Count(); i++) + { + var element = colliderElements.ElementAt(i); + string name = i > 0 ? "Secondary Collider" : "Main Collider"; + Colliders.Add(new ColliderParams(element, this, name)); + } } } protected void CreateLimbs() { Limbs.Clear(); - foreach (var element in MainElement.GetChildElements("limb")) + if (MainElement?.GetChildElements("limb") is { } childElements) { - Limbs.Add(new LimbParams(element, this)); + foreach (var element in childElements) + { + Limbs.Add(new LimbParams(element, this)); + } } Limbs = Limbs.OrderBy(l => l.ID).ToList(); } @@ -430,8 +436,8 @@ namespace Barotrauma copy.Serialize(); Memento.Store(copy); } - public void Undo() => RevertTo(Memento.Undo() as RagdollParams); - public void Redo() => RevertTo(Memento.Redo() as RagdollParams); + public void Undo() => RevertTo(Memento.Undo()); + public void Redo() => RevertTo(Memento.Redo()); public void ClearHistory() => Memento.Clear(); private void RevertTo(RagdollParams source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 1881758e9..77cc53bad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -22,13 +22,13 @@ namespace Barotrauma.Abilities private static readonly List WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast().ToList(); private readonly string itemIdentifier; - private readonly string[] tags; + private readonly Identifier[] tags; private readonly WeaponType weapontype; private readonly bool ignoreNonHarmfulAttacks; public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty); - tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty()); ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index a71667245..f92523e10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -41,10 +41,10 @@ namespace Barotrauma.Abilities if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } - foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + foreach (var reputationReward in mission.ReputationRewards) { - if (amount <= 0) { continue; } - if (GetMatchingFaction(factionIdentifier) is { } faction && + if (reputationReward.Amount <= 0) { continue; } + if (GetMatchingFaction(reputationReward.FactionIdentifier) is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { return CheckMissionType(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs new file mode 100644 index 000000000..b1598065c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs @@ -0,0 +1,22 @@ + +namespace Barotrauma.Abilities +{ + class AbilityConditionAllyHasTalent : AbilityConditionDataless + { + private readonly Identifier talentIdentifier; + + public AbilityConditionAllyHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific() + { + foreach (Character crewCharacter in Character.GetFriendlyCrew(characterTalent.Character)) + { + if (crewCharacter.HasTalent(talentIdentifier)) { return true; } + } + return false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 8b20847b1..7d75c9f7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -5,12 +5,12 @@ namespace Barotrauma.Abilities { class AbilityConditionHasItem : AbilityConditionDataless { - private readonly string[] tags; + private readonly Identifier[] tags; readonly bool requireAll; public AbilityConditionHasItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tags = conditionElement.GetAttributeStringArray("tags", Array.Empty()); + tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty()); requireAll = conditionElement.GetAttributeBool("requireall", false); } @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities if (requireAll) { - foreach (string tag in tags) + foreach (Identifier tag in tags) { if (character.GetEquippedItem(tag) == null) { return false; } } @@ -31,7 +31,7 @@ namespace Barotrauma.Abilities } else { - foreach (string tag in tags) + foreach (Identifier tag in tags) { if (character.GetEquippedItem(tag) != null) { return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs index 1b8d54fc3..2ee0a66ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -5,13 +5,13 @@ namespace Barotrauma.Abilities { class AbilityConditionHasStatusTag : AbilityConditionDataless { - private readonly string tag; + private readonly Identifier tag; public AbilityConditionHasStatusTag(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tag = conditionElement.GetAttributeString("tag", ""); - if (string.IsNullOrEmpty(tag)) + tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty); + if (tag.IsEmpty) { DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag."); } @@ -19,7 +19,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - if (!string.IsNullOrEmpty(tag)) + if (!tag.IsEmpty) { return StatusEffect.DurationList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)) || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs index de2f98107..0912d9ba4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs @@ -12,8 +12,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - bool result = character.HasTalent(talentIdentifier); - return result; + return character.HasTalent(talentIdentifier); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs index 5c6c201de..00eb57b23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs @@ -27,8 +27,8 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless return false; static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) => - character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null || - character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null; + character.GetEquippedItem(tagOrIdentifier, InvSlotType.RightHand) is not null || + character.GetEquippedItem(tagOrIdentifier, InvSlotType.LeftHand) is not null; } } \ 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 4185832c4..147725f90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -37,10 +37,11 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - if (Character?.Submarine is null) { return; } + if (Character is null) { return; } - foreach (Item item in Character.Submarine.GetItems(true)) + foreach (Item item in Item.ItemList) { + if (item.Submarine?.TeamID != Character.TeamID) { continue; } if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) { item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index b62f56296..a2a94cf37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -66,12 +66,12 @@ { foreach (Character c in Character.GetFriendlyCrew(Character)) { - c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + c?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } else { - Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index 29f0cf0cb..b17432528 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - IEnumerable enemyCharacters = Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.None); + IEnumerable enemyCharacters = Character.CharacterList.Where(c => !Character.IsFriendly(c)); int timesGiven = 0; foreach (Character enemyCharacter in enemyCharacters) @@ -27,7 +27,6 @@ namespace Barotrauma.Abilities if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; } if (enemyCharacter.IsDead) { continue; } if (!enemyCharacter.LockHands) { continue; } - if (timesGiven > max) { continue; } Character.GiveMoney(moneyAmount); GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); foreach (Character character in Character.GetFriendlyCrew(Character)) @@ -35,6 +34,7 @@ namespace Barotrauma.Abilities character.Info?.GiveExperience(experienceAmount); } timesGiven++; + if (max > 0 && timesGiven >= max) { break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs index 75d474784..74f578938 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -5,32 +5,43 @@ namespace Barotrauma.Abilities { class CharacterAbilityRegenerateLoot : CharacterAbility { + /// + /// Chance for the loot to be regenerated. We can't use for this, + /// because it'd allow the player to reopen the container until the ability is executed successfully + /// + private readonly float randomChance; + // separate random chance used for the ability itself to prevent the player // from opening/reopening a container until it spawns loot - private readonly float randomChance; + + /// + /// Chance for an individual loot item to be generated. + /// + private readonly float randomChancePerItem = 1.0f; // not maintained through death, so it's possible for players to respawn and re-loot chests // seems like a minor issue for now - private readonly List openedContainers = new List(); + private readonly HashSet openedContainers = new HashSet(); public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); + randomChance = abilityElement.GetAttributeFloat(nameof(randomChance), 1f); + randomChancePerItem = abilityElement.GetAttributeFloat(nameof(randomChancePerItem), 1f); } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityItem)?.Item is Item item) - { - if (openedContainers.Contains(item)) { return; } - openedContainers.Add(item); - if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } + if ((abilityObject as IAbilityItem)?.Item is not Item item) { return; } + if (openedContainers.Contains(item)) { return; } - if (item.GetComponent() is ItemContainer itemContainer) - { - AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer); - } + openedContainers.Add(item); + if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } + + if (item.GetComponent() is ItemContainer itemContainer) + { + AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer, skipItemProbability: 1.0f - randomChancePerItem); } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index ff1e5ccde..2ac8648c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -5,10 +5,10 @@ namespace Barotrauma.Abilities class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly { // this should just be its own class, misleading to inherit here - private readonly string tag; + private readonly Identifier tag; public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - tag = abilityElement.GetAttributeString("tag", ""); + tag = abilityElement.GetAttributeIdentifier("tag", Identifier.Empty); } protected override void ApplyEffect() @@ -37,7 +37,7 @@ namespace Barotrauma.Abilities ApplyEffectSpecific(closestCharacter); } - static bool SelectedItemHasTag(Character character, string tag) => + static bool SelectedItemHasTag(Character character, Identifier tag) => (character.SelectedItem != null && character.SelectedItem.HasTag(tag)) || (character.SelectedSecondaryItem != null && character.SelectedSecondaryItem.HasTag(tag)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index d14c9df8e..2c1867589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -133,6 +133,14 @@ namespace Barotrauma if (IsTalentLocked(talentIdentifier)) { return false; } + if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier)) + { + //if the character already has the talent, it must be viable? + //needed for backwards compatibility, otherwise if we remove e.g. a tier 1 or tier 2 talent, + //all the already-unlocked higher-tier talents will be considered invalid which'll break the talent selection + return true; + } + foreach (var subTree in talentTree!.TalentSubTrees) { if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; } @@ -143,11 +151,12 @@ namespace Barotrauma { return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); } + //if a previous stage hasn't been completed, this talent can't be selected yet bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents); if (!optionStageCompleted) { break; - } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs new file mode 100644 index 000000000..eaa828c56 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs @@ -0,0 +1,84 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class CircuitBoxComponent : CircuitBoxNode, ICircuitBoxIdentifiable + { + public readonly Item Item; + public ushort ID { get; } + + public readonly ItemPrefab UsedResource; + + public CircuitBoxComponent(ushort id, Item item, Vector2 position, CircuitBox circuitBox, ItemPrefab usedResource): base(circuitBox) + { + if (item.Connections is null) + { + throw new ArgumentNullException(nameof(item.Connections), $"Tried to load a CircuitBoxNode with an item \"{item.Prefab.Name}\" that has no connections."); + } + + var conns = item.Connections.Select(connection => new CircuitBoxNodeConnection(Vector2.Zero, this, connection, circuitBox)).ToList(); + + Vector2 size = CalculateSize(conns); + + ID = id; + Item = item; + Size = size; + Connectors = conns.Cast().ToImmutableArray(); + Position = position; + UsedResource = usedResource; + UpdatePositions(); + } + + public static Option TryLoadFromXML(ContentXElement element, CircuitBox circuitBox) + { + ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID); + Vector2 position = element.GetAttributeVector2("position", Vector2.Zero); + var itemIdOption = ItemSlotIndexPair.TryDeserializeFromXML(element, "backingitemid"); + Identifier usedResourceIdentifier = element.GetAttributeIdentifier("usedresource", Identifier.Empty); + + if (!itemIdOption.TryUnwrap(out var itemId) || itemId.FindItemInContainer(circuitBox.ComponentContainer) is not { } backingItem) + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxComponent.TryLoadFromXML:IdNotFound", + $"Failed to find item with ID {itemId} for CircuitBoxNode with ID {id}"); + return Option.None; + } + + if (!ItemPrefab.Prefabs.TryGet(usedResourceIdentifier, out var usedResource)) + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxComponent.TryLoadXML:UsedResourceNotFound", + $"Failed to find item prefab with identifier {usedResourceIdentifier} for CircuitBoxNode with ID {id}"); + return Option.None; + } + + return Option.Some(new CircuitBoxComponent(id, backingItem, position, circuitBox, usedResource)); + } + + public XElement Save() + { + return new XElement("Component", + new XAttribute("id", ID), + new XAttribute("position", XMLExtensions.Vector2ToString(Position)), + new XAttribute("backingitemid", ItemSlotIndexPair.Serialize(Item)), + new XAttribute("usedresource", UsedResource.Identifier)); + } + + public void Remove() + { + if (Entity.Spawner is { } spawner && Screen.Selected is not { IsEditor: true }) + { + spawner.AddEntityToRemoveQueue(Item); + return; + } + + // if EntitySpawner is not available + Item.Remove(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs new file mode 100644 index 000000000..54eb1ef7e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs @@ -0,0 +1,121 @@ +#nullable enable + +using System.Collections.Generic; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + + internal class CircuitBoxInputConnection : CircuitBoxConnection + { + /// + /// Circuit box input connection is classified as an output because it behaves like an output inside the circuit box. + /// As in you can connect it to a input pin of a component. + /// + public override bool IsOutput => true; + public readonly List ExternallyConnectedTo = new(); + + public CircuitBoxInputConnection(Vector2 position, Connection connection, CircuitBox box) : base(position, connection, box) { } + + public override void ReceiveSignal(Signal signal) + { + foreach (CircuitBoxConnection connector in ExternallyConnectedTo) + { + if (connector is CircuitBoxOutputConnection output) + { + output.ReceiveSignal(signal); + return; + } + Connection.SendSignalIntoConnection(signal, connector.Connection); + } + } + } + + internal class CircuitBoxOutputConnection : CircuitBoxConnection + { + /// + /// Circuit box output connection is classified as an input because it behaves like an input inside the circuit box. + /// As in you can connect it to a output pin of a component. + /// + public override bool IsOutput => false; + + public CircuitBoxOutputConnection(Vector2 position, Connection connection, CircuitBox circuitBox) : base(position, connection, circuitBox) { } + + public override void ReceiveSignal(Signal signal) => CircuitBox.Item.SendSignal(signal, Connection); + } + + internal class CircuitBoxNodeConnection : CircuitBoxConnection + { + public override bool IsOutput => Connection.IsOutput; + + public CircuitBoxComponent Component; + + public bool HasAvailableSlots => Connection.WireSlotsAvailable(); + + public CircuitBoxNodeConnection(Vector2 position, CircuitBoxComponent component, Connection connection, CircuitBox circuitBox) : base(position, connection, circuitBox) + { + Component = component; + } + + public override void ReceiveSignal(Signal signal) => Connection.SendSignalIntoConnection(signal, Connection); + } + + internal abstract partial class CircuitBoxConnection + { + public readonly Connection Connection; + + public abstract bool IsOutput { get; } + + public RectangleF Rect; + + private Vector2 position; + + public List ExternallyConnectedFrom = new(); + + public static float Size = CircuitBoxSizes.ConnectorSize; + + public Vector2 Position + { + get => position; + set + { + Rect.X = value.X - Rect.Width / 2f; + Rect.Y = value.Y - Rect.Height / 2f; + position = value; + } + } + + public float Length { get; private set; } + + public Vector2 AnchorPoint + => new Vector2(IsOutput ? Rect.Right + CircuitBoxSizes.AnchorOffset : Rect.Left - CircuitBoxSizes.AnchorOffset, Rect.Center.Y); + + public readonly CircuitBox CircuitBox; + + protected CircuitBoxConnection(Vector2 position, Connection connection, CircuitBox circuitBox) + { + Connection = connection; + Rect.Width = Rect.Height = Size; + Position = position; + CircuitBox = circuitBox; + InitProjSpecific(circuitBox); + } + + private partial void InitProjSpecific(CircuitBox circuitBox); + + public abstract void ReceiveSignal(Signal signal); + + public bool Contains(Vector2 pos) + { + float x = Rect.X, + y = -(Rect.Y + Rect.Height), + width = Rect.Width, + height = Rect.Height; + + RectangleF rect = new(x, y, width, height); + + return rect.Contains(pos); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs new file mode 100644 index 000000000..1340152a0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs @@ -0,0 +1,109 @@ +#nullable enable + +using System; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + [NetworkSerialize] + internal readonly record struct NetCircuitBoxCursorInfo(Vector2[] RecordedPositions, Option DragStart, Option HeldItem, ushort CharacterID = 0) : INetSerializableStruct; + + internal sealed class CircuitBoxCursor + { + public NetCircuitBoxCursorInfo Info; + + public CircuitBoxCursor(NetCircuitBoxCursorInfo info) + { + if (Entity.FindEntityByID(info.CharacterID) is Character c) + { + Color = GenerateColor(c.Name); + } + + UpdateInfo(info); + } + + public void UpdateInfo(NetCircuitBoxCursorInfo newInfo) + { + Info = newInfo; + + newInfo.HeldItem.Match( + some: newIdentifier => + HeldPrefab.Match( + some: oldPrefab => + { + if (oldPrefab.Identifier == newIdentifier) { return; } + SetHeldPrefab(newIdentifier); + }, + none: () => SetHeldPrefab(newIdentifier) + ), + none: () => HeldPrefab = Option.None); + + prevPosition = DrawPosition; + + void SetHeldPrefab(Identifier identifier) + { + ItemPrefab? prefab = ItemPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier)); + HeldPrefab = prefab is null ? Option.None : Option.Some(prefab); + } + } + + public Option HeldPrefab { get; private set; } = Option.None; + + public Color Color = Color.White; + + public static Color GenerateColor(string name) + { + Random random = new Random(ToolBox.StringToInt(name)); + return ToolBox.HSVToRGB(random.NextSingle() * 360f, 1f, 1f); + } + + private const float UpdateTimeout = 5f; + + private float updateTimer; + private float positionTimer; + private Vector2 prevPosition; + public Vector2 DrawPosition; + + public bool IsActive => updateTimer < UpdateTimeout; + + public void Update(float deltaTime) + { + updateTimer += deltaTime; + + Vector2 finalPosition = Info.RecordedPositions[^1]; + + if (positionTimer > 1f) + { + DrawPosition = finalPosition; + prevPosition = Vector2.Zero; + } + else + { + positionTimer += deltaTime; + + float stepTimer = positionTimer * 10f; + int targetPositonIndex = (int)MathF.Floor(stepTimer); + int prevPosIndex = targetPositonIndex - 1; + + Vector2 targetPosition = IsInRange(targetPositonIndex, Info.RecordedPositions.Length) + ? Info.RecordedPositions[targetPositonIndex] + : finalPosition; + + Vector2 prevTargetPosition = IsInRange(prevPosIndex, Info.RecordedPositions.Length) + ? Info.RecordedPositions[prevPosIndex] + : prevPosition; + + DrawPosition = Vector2.Lerp(prevTargetPosition, targetPosition, MathHelper.Clamp(stepTimer % 1f, 0f, 1f)); + } + + static bool IsInRange(int index, int length) + => index >= 0 && index < length; + } + + public void ResetTimers() + { + positionTimer = 0f; + updateTimer = 0f; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs new file mode 100644 index 000000000..4c935f53e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -0,0 +1,38 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal sealed class CircuitBoxInputOutputNode : CircuitBoxNode + { + public enum Type + { + Invalid, + Input, + Output + } + + public Type NodeType; + + public CircuitBoxInputOutputNode(IReadOnlyList conns, Vector2 initialPosition, Type type, CircuitBox circuitBox): base(circuitBox) + { + Size = CalculateSize(conns); + Connectors = conns.ToImmutableArray(); + Position = initialPosition; + NodeType = type; + UpdatePositions(); + } + + public XElement Save() => new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position))); + + public void Load(ContentXElement element) + { + Position = element.GetAttributeVector2("pos", Vector2.Zero); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs new file mode 100644 index 000000000..cab96ebee --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs @@ -0,0 +1,163 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public enum CircuitBoxOpcode + { + Error, + Cursor, + AddComponent, + MoveComponent, + AddWire, + RemoveWire, + SelectComponents, + SelectWires, + UpdateSelection, + DeleteComponent, + ServerInitialize + } + + [NetworkSerialize] + internal readonly record struct NetCircuitBoxHeader(CircuitBoxOpcode Opcode, ushort ItemID, byte ComponentIndex) : INetSerializableStruct + { + public Option FindTarget() => CircuitBox.FindCircuitBox(ItemID, ComponentIndex); + } + + [NetworkSerialize] + internal readonly record struct CircuitBoxConnectorIdentifier(Identifier SignalConnection, Option TargetId) : INetSerializableStruct + { + public static CircuitBoxConnectorIdentifier FromConnection(CircuitBoxConnection connection) => + connection switch + { + (CircuitBoxInputConnection or CircuitBoxOutputConnection) + => new CircuitBoxConnectorIdentifier(connection.Name.ToIdentifier(), Option.None), + + CircuitBoxNodeConnection nodeConnection + => new CircuitBoxConnectorIdentifier(connection.Name.ToIdentifier(), Option.Some(nodeConnection.Component.ID)), + + _ => throw new ArgumentOutOfRangeException(nameof(connection)) + }; + + public Option FindConnection(CircuitBox circuitBox) + { + if (!TargetId.TryUnwrap(out var id)) + { + return circuitBox.FindInputOutputConnection(SignalConnection); + } + + foreach (CircuitBoxComponent boxNode in circuitBox.Components) + { + if (boxNode.ID != id) { continue; } + + foreach (var conn in boxNode.Connectors) + { + if (conn.Name != SignalConnection) { continue; } + + return Option.Some(conn); + } + } + + return Option.None; + } + + public XElement Save(string name) => new XElement(name, + new XAttribute("name", SignalConnection), + new XAttribute("target", TargetId.TryUnwrap(out var value) ? value.ToString() : string.Empty)); + + public static CircuitBoxConnectorIdentifier Load(ContentXElement element) + { + string? name = element.GetAttributeString("name", string.Empty); + string? target = element.GetAttributeString("target", string.Empty); + + Option targetId = Option.None; + if (!string.IsNullOrWhiteSpace(target)) + { + targetId = ushort.TryParse(target, out var value) ? Option.Some(value) : Option.None; + } + + return new CircuitBoxConnectorIdentifier(name.ToIdentifier(), targetId); + } + + public override string ToString() + => $"{{Name: {SignalConnection}, ID: {(TargetId.TryUnwrap(out var value) ? value.ToString() : "N/A")}}}"; + } + + [NetworkSerialize] + internal readonly record struct CircuitBoxAddComponentEvent(UInt32 PrefabIdentifier, Vector2 Position) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerCreateComponentEvent(ushort BackingItemId, UInt32 UsedResource, ushort ComponentId, Vector2 Position) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxRemoveComponentEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray TargetIDs, ImmutableArray IOs, Vector2 MoveAmount) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray TargetIDs, ImmutableArray IOs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray ComponentIds, ImmutableArray WireIds, ImmutableArray InputOutputs) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxIdSelectionPair(ushort ID, Option SelectedBy) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxTypeSelectionPair(CircuitBoxInputOutputNode.Type Type, Option SelectedBy) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxSelectWiresEvent(ImmutableArray TargetIDs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxClientAddWireEvent(Color Color, CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, UInt32 SelectedWirePrefabIdentifier) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerCreateWireEvent(CircuitBoxClientAddWireEvent Request, ushort WireId, Option BackingItemId) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxRemoveWireEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxInitializeStateFromServerEvent( + ImmutableArray Components, + ImmutableArray Wires, + Vector2 InputPos, + Vector2 OutputPos) : INetSerializableStruct; + + internal readonly record struct CircuitBoxEventData(INetSerializableStruct Data) : ItemComponent.IEventData + { + public CircuitBoxOpcode Opcode => + Data switch + { + (CircuitBoxAddComponentEvent or CircuitBoxServerCreateComponentEvent) + => CircuitBoxOpcode.AddComponent, + CircuitBoxRemoveComponentEvent + => CircuitBoxOpcode.DeleteComponent, + CircuitBoxMoveComponentEvent + => CircuitBoxOpcode.MoveComponent, + CircuitBoxSelectNodesEvent + => CircuitBoxOpcode.SelectComponents, + CircuitBoxSelectWiresEvent + => CircuitBoxOpcode.SelectWires, + CircuitBoxServerUpdateSelection + => CircuitBoxOpcode.UpdateSelection, + (CircuitBoxClientAddWireEvent or CircuitBoxServerCreateWireEvent) + => CircuitBoxOpcode.AddWire, + CircuitBoxRemoveWireEvent + => CircuitBoxOpcode.RemoveWire, + CircuitBoxInitializeStateFromServerEvent + => CircuitBoxOpcode.ServerInitialize, + _ => throw new ArgumentOutOfRangeException(nameof(Data)) + }; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs new file mode 100644 index 000000000..0dbb018f8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs @@ -0,0 +1,132 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class CircuitBoxNode : CircuitBoxSelectable + { + public Vector2 Size; + public RectangleF Rect; + private Vector2 position; + + public Vector2 Position + { + get => position; + set + { + const float clampSize = CircuitBoxSizes.PlayableAreaSize / 2f; + + position = new Vector2(Math.Clamp(value.X, -clampSize, clampSize), + Math.Clamp(value.Y, -clampSize, clampSize)); + UpdatePositions(); + } + } + + public ImmutableArray Connectors; + + public static float Opacity = 0.8f; + + public readonly CircuitBox CircuitBox; + + public CircuitBoxNode(CircuitBox circuitBox) + { + CircuitBox = circuitBox; + } + + public static Vector2 CalculateSize(IReadOnlyList conns) + { + Vector2 leftSize = Vector2.Zero, + rightSize = Vector2.Zero; + + foreach (var c in conns) + { + if (c.IsOutput) + { + rightSize.X = MathF.Max(rightSize.X, c.Length); + } + else + { + leftSize.X = MathF.Max(leftSize.X, c.Length); + } + + if (c.IsOutput) + { + rightSize.Y += CircuitBoxConnection.Size; + } + else + { + leftSize.Y += CircuitBoxConnection.Size; + } + } + + return new Vector2(leftSize.X + CircuitBoxSizes.NodeInitialPadding + rightSize.X, CircuitBoxSizes.NodeInitialPadding + MathF.Max(leftSize.Y, rightSize.Y)); + } + + protected void UpdatePositions() + { + Vector2 rectStart = Position - Size / 2f; + Vector2 rectSize = Size; + rectSize.Y += CircuitBoxSizes.NodeHeaderHeight; + Rect = new RectangleF(rectStart, rectSize); + +#if CLIENT + UpdateDrawRects(); +#endif + + int leftIndex = 0, + rightIndex = 0; + + int inputCount = 0, + outputCount = 0; + + foreach (var c in Connectors) + { + if (c.IsOutput) + { + outputCount++; + } + else + { + inputCount++; + } + } + + Vector2 drawPos = Position; + drawPos.Y = -drawPos.Y; + + foreach (var c in Connectors) + { + bool isOutput = c.IsOutput; + + int yIndex = isOutput ? rightIndex : leftIndex; + int count = isOutput ? outputCount : inputCount; + + float totalHeight = (count * CircuitBoxConnection.Size) / 2f; + float y = (yIndex * CircuitBoxConnection.Size) - totalHeight; + + float halfWidth = Rect.Width / 2f - CircuitBoxConnection.Size / 2f; + + halfWidth -= 16f; + + float xOffset = c.IsOutput ? halfWidth : -halfWidth; + + Vector2 inputPos = drawPos + new Vector2(xOffset, y + c.Rect.Height / 2f); + c.Position = inputPos; + + if (isOutput) + { + rightIndex++; + } + else + { + leftIndex++; + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs new file mode 100644 index 000000000..913f1756b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; + +namespace Barotrauma +{ + internal class CircuitBoxSelectable + { + public bool IsSelected; + public ushort SelectedBy; + + public bool IsSelectedByMe + { + get + { + if (GameMain.NetworkMember is { IsServer: true }) + { + throw new Exception("CircuitBoxSelectable.IsSelectedByMe should never be used by the server."); + } + + if (Character.Controlled is { } controlled) + { + return SelectedBy == controlled.ID; + } + + return false; + } + } + + public void SetSelected(Option selectedBy) + { + if (selectedBy.TryUnwrap(out ushort id)) + { + SelectedBy = id; + IsSelected = true; + return; + } + + IsSelected = false; + SelectedBy = 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs new file mode 100644 index 000000000..5f7f211b8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Barotrauma +{ + internal static class CircuitBoxSizes + { + public const int ConnectorSize = 32; + public const int AnchorOffset = 24; + public const int NodeHeaderHeight = 48; + public const int NodeInitialPadding = 64; + public const int WireWidth = 10; + public const int WireKnobLength = 16; + public const int NodeHeaderTextPadding = 8; + + public const float PlayableAreaSize = 8192f; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs new file mode 100644 index 000000000..843b873fb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs @@ -0,0 +1,186 @@ +#nullable enable + +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class CircuitBoxWire : CircuitBoxSelectable, ICircuitBoxIdentifiable + { + public CircuitBoxConnection From, To; + public readonly Option BackingWire; + + public readonly Color Color; + public readonly ItemPrefab UsedItemPrefab; + + public ushort ID { get; } + + public CircuitBoxWire(CircuitBox circuitBox, ushort Id, Option backingItem, CircuitBoxConnection from, CircuitBoxConnection to, ItemPrefab prefab) + { + ID = Id; + From = from; + To = to; + BackingWire = backingItem; + Color = prefab.SpriteColor; + UsedItemPrefab = prefab; +#if CLIENT + Renderer = new CircuitBoxWireRenderer(Option.Some(this), to.AnchorPoint, from.AnchorPoint, Color, circuitBox.WireSprite); +#endif + EnsureWireConnected(); + } + + public XElement Save() + { + XElement element = new XElement("Wire", + new XAttribute("id", ID), + new XAttribute("backingitemid", BackingWire.TryUnwrap(out var item) ? ItemSlotIndexPair.Serialize(item) : string.Empty), + new XAttribute("prefab", UsedItemPrefab.Identifier)); + + XElement fromElement = CircuitBoxConnectorIdentifier.FromConnection(From).Save("From"), + toElement = CircuitBoxConnectorIdentifier.FromConnection(To).Save("To"); + + element.Add(fromElement); + element.Add(toElement); + + return element; + } + + public static Option TryLoadFromXML(ContentXElement element, CircuitBox circuitBox) + { + ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID); + var backingItemIdOption = ItemSlotIndexPair.TryDeserializeFromXML(element, "backingitemid"); + Identifier usedPrefabIdentifier = element.GetAttributeIdentifier("prefab", Identifier.Empty); + + if (!ItemPrefab.Prefabs.TryGet(usedPrefabIdentifier, out var itemPrefab)) + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:PrefabNotFound", + $"Failed to find prefab used to create wire with identifier {usedPrefabIdentifier} for CircuitBoxWire with ID {id}"); + return Option.None; + } + + Option backingItem = Option.None; + if (backingItemIdOption.TryUnwrap(out var backingItemIdPair)) + { + if (backingItemIdPair.FindItemInContainer(circuitBox.WireContainer) is { } item) + { + backingItem = Option.Some(item); + } + else + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:IdNotFound", + $"Failed to find item with ID {backingItemIdPair} for CircuitBoxWire with ID {id}"); + return Option.None; + } + } + + Option From = Option.None, + To = Option.None; + + foreach (ContentXElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "from": + var fromIdentifier = CircuitBoxConnectorIdentifier.Load(subElement); + if (fromIdentifier.FindConnection(circuitBox).TryUnwrap(out var fromConnection)) + { + From = Option.Some(fromConnection); + } + break; + case "to": + var toIdentifier = CircuitBoxConnectorIdentifier.Load(subElement); + if (toIdentifier.FindConnection(circuitBox).TryUnwrap(out var toConnection)) + { + To = Option.Some(toConnection); + } + break; + } + } + + if (From.TryUnwrap(out var from) && To.TryUnwrap(out var to)) + { + return Option.Some(new CircuitBoxWire(circuitBox, id, backingItem, from, to, itemPrefab)); + } + + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:MissingFromOrTo", + $"Failed to load CircuitBoxWire with ID {id}, missing \"From\" or \"To\" connection."); + + return Option.None; + } + + public void EnsureWireConnected() + { + EnsureExternalConnection(From, To); + EnsureExternalConnection(To, From); + + if (!BackingWire.TryUnwrap(out var item) || item.GetComponent() is not { } wire) { return; } + + wire.DropOnConnect = false; + + From.Connection.ConnectWire(wire); + To.Connection.ConnectWire(wire); + + wire.Connect(From.Connection, 0, addNode: false, sendNetworkEvent: false); + wire.Connect(To.Connection, 1, addNode: false, sendNetworkEvent: false); + + static void EnsureExternalConnection(CircuitBoxConnection one, CircuitBoxConnection two) + { + switch (one) + { + case CircuitBoxInputConnection input: + { + if (input.ExternallyConnectedTo.Contains(two)) { break; } + input.ExternallyConnectedTo.Add(two); + break; + } + case CircuitBoxOutputConnection output: + { + if (output.ExternallyConnectedFrom.Contains(two)) { break; } + output.ExternallyConnectedFrom.Add(two); + break; + } + case CircuitBoxNodeConnection node when two is CircuitBoxOutputConnection output: + { + if (node.Connection.CircuitBoxConnections.Contains(output)) { break; } + node.Connection.CircuitBoxConnections.Add(output); + break; + } + case CircuitBoxNodeConnection node when two is CircuitBoxInputConnection input: + { + if (node.ExternallyConnectedFrom.Contains(input)) { break; } + node.ExternallyConnectedFrom.Add(input); + break; + } + } + } + } + + public void Remove() + { + // client should not remove wires + if (GameMain.NetworkMember is { IsClient: true }) { return; } + + if (!BackingWire.TryUnwrap(out var wireItem)) { return; } + + if (Entity.Spawner is { } spawner && Screen.Selected is not { IsEditor: true }) + { + spawner.AddEntityToRemoveQueue(wireItem); + return; + } + + Wire? wire = wireItem.GetComponent(); + if (wire is not null) + { + From.Connection.DisconnectWire(wire); + To.Connection.DisconnectWire(wire); + } + // if EntitySpawner is not available + wireItem.Remove(); + } + + public static ItemPrefab DefaultWirePrefab => ItemPrefab.Prefabs[Tags.RedWire]; + public static ItemPrefab SelectedWirePrefab = DefaultWirePrefab; + public static readonly Color DefaultWireColor = DefaultWirePrefab.SpriteColor; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs new file mode 100644 index 000000000..4b86da1c0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public interface ICircuitBoxIdentifiable + { + public const ushort NullComponentID = ushort.MaxValue; + + public ushort ID { get; } + + public static ushort FindFreeID(IReadOnlyCollection ids) where T : ICircuitBoxIdentifiable + { + var sortedIds = ids.Select(static i => i.ID).ToImmutableHashSet(); + + for (ushort i = 0; i < NullComponentID - 1; i++) + { + if (!sortedIds.Contains(i)) + { + return i; + } + } + + return NullComponentID; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs new file mode 100644 index 000000000..c2ccdb8bb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs @@ -0,0 +1,44 @@ +#nullable enable + +using System; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal readonly record struct ItemSlotIndexPair(int Slot, int StackIndex) + { + public static Option TryDeserializeFromXML(ContentXElement element, string elementName) + { + string? elementStr = element.GetAttributeString(elementName, string.Empty); + if (string.IsNullOrEmpty(elementStr)) { return Option.None; } + + var point = XMLExtensions.ParsePoint(elementStr); + return Option.Some(new ItemSlotIndexPair(point.X, point.Y)); + } + + public static string Serialize(Item item) + { + Inventory parent = item.ParentInventory; + if (item.ParentInventory is null) + { + throw new Exception($"Item \"{item.Name}\" is not in an inventory."); + } + + int slotIndex = parent.FindIndex(item); + + int stackIndex = parent.GetItemStackSlotIndex(item, slotIndex); + + if (slotIndex < 0 || stackIndex < 0) + { + throw new Exception($"Unable to find item \"{item.Name}\" in its parent inventory."); + } + + return XMLExtensions.PointToString(new Point(slotIndex, stackIndex)); + } + + public Item? FindItemInContainer(ItemContainer? container) + => container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 15d0bb76a..f2470ed6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -67,6 +67,22 @@ namespace Barotrauma .ToImmutableHashSet(); } + public static bool IsLegacyContentType(XElement contentFileElement, ContentPackage package, bool logWarning) + { + Identifier elemName = contentFileElement.NameAsIdentifier(); + if (elemName == "TraitorMissions") + { + if (logWarning) + { + DebugConsole.AddWarning( + $"The content type \"TraitorMission\" in content package \"{package.Name}\" is no longer supported." + + $" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents."); + } + return true; + } + return false; + } + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { static Result fail(string error, Exception? exception = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs index 6f636c4e8..4c25ad989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -28,8 +28,16 @@ namespace Barotrauma { foreach (var subElement in parentElement.Elements()) { - var prefab = new EventPrefab(subElement, this); - EventPrefab.Prefabs.Add(prefab, overriding); + if (subElement.NameAsIdentifier() == "traitorevent") + { + var prefab = new TraitorEventPrefab(subElement, this); + EventPrefab.Prefabs.Add(prefab, overriding); + } + else + { + var prefab = new EventPrefab(subElement, this); + EventPrefab.Prefabs.Add(prefab, overriding); + } } } else if (elemName == "eventsprites") diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs index 22c40b0a8..f3a777cb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs @@ -28,6 +28,10 @@ namespace Barotrauma { //Overrides for subs don't exist! Should we change this? } + + // Use byte-perfect hash because this is compressed, trimming whitespace is incorrect and needlessly slow here + public override Md5Hash CalculateHash() + => Md5Hash.CalculateForFile(Path.FullPath, Md5Hash.StringHashOptions.BytePerfect); } [NotSyncedInMultiplayer] diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs deleted file mode 100644 index a43c10379..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Xml.Linq; - -#warning TODO: This file is just about the only thing that's actually somewhat okay about the current traitor system. Gut the whole thing. - -#if CLIENT -using PrefabType = Barotrauma.TraitorMissionPrefab; -#elif SERVER -using PrefabType = Barotrauma.TraitorMissionPrefab.TraitorMissionEntry; -#endif - -namespace Barotrauma -{ - [RequiredByCorePackage] - sealed class TraitorMissionsFile : GenericPrefabFile - { - public TraitorMissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } - - protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission"; - protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions"; - protected override PrefabCollection Prefabs => PrefabType.Prefabs; - protected override PrefabType CreatePrefab(ContentXElement element) - { - return new PrefabType(element, this); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 01cd9319c..3d78d1da8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -23,7 +23,7 @@ namespace Barotrauma : string.Empty); } - public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 13, 2); + public static readonly Version MinimumHashCompatibleVersion = new Version(1, 1, 0, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -110,6 +110,7 @@ namespace Barotrauma InstallTime = rootElement.GetAttributeDateTime("installtime"); var fileResults = rootElement.Elements() + .Where(e => !ContentFile.IsLegacyContentType(e, this, logWarning: true)) .Select(e => ContentFile.CreateFromXElement(this, e)) .ToArray(); @@ -337,6 +338,7 @@ namespace Barotrauma XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); var fileResults = rootElement.Elements() + .Where(e => !ContentFile.IsLegacyContentType(e, this, logWarning: true)) .Select(e => ContentFile.CreateFromXElement(this, e)) .ToArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 3f3249990..f113e04f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -32,6 +32,11 @@ namespace Barotrauma private static readonly List regular = new List(); public static IReadOnlyList Regular => regular; + /// + /// Combined hash of the currently enabled packages. Can be used to check if the enabled mods or their load order has changed. + /// + public static Md5Hash MergedHash { get; private set; } = Md5Hash.Blank; + public static IEnumerable All => Core != null ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular) @@ -149,6 +154,7 @@ namespace Barotrauma .SelectMany(r => r.Files) .Distinct(new TypeComparer()) .ForEach(f => f.Sort()); + MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash)); } public static int IndexOf(ContentPackage contentPackage) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 18da93f92..149395221 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -1,10 +1,9 @@ #nullable enable +using Barotrauma.IO; using System; -using System.Globalization; using System.Linq; using System.Text.RegularExpressions; -using Barotrauma.IO; namespace Barotrauma { @@ -93,10 +92,21 @@ namespace Barotrauma } public static ContentPath FromRaw(string? rawValue) - => new ContentPath(null, rawValue); - + => FromRaw(null, rawValue); + + private static ContentPath? prevCreatedRaw; + public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue) - => new ContentPath(contentPackage, rawValue); + { + var newRaw = new ContentPath(contentPackage, rawValue); + if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage && + prevCreatedRaw.RawValue == rawValue) + { + newRaw.cachedValue = prevCreatedRaw.Value; + } + prevCreatedRaw = newRaw; + return newRaw; + } public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue) { @@ -146,8 +156,8 @@ namespace Barotrauma return HashCode.Combine(RawValue, ContentPackage, cachedValue, cachedFullPath); } - public bool IsNullOrEmpty() => string.IsNullOrEmpty(Value); - public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value); + public bool IsPathNullOrEmpty() => string.IsNullOrEmpty(Value); + public bool IsPathNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value); public bool EndsWith(string suffix) => Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 74937c8ce..c97b534ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -52,7 +52,7 @@ namespace Barotrauma => Element.Descendants().Select(e => new ContentXElement(ContentPackage, e)); public IEnumerable GetChildElements(string name) - => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.InvariantCultureIgnoreCase)); + => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.OrdinalIgnoreCase)); public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); @@ -76,6 +76,7 @@ namespace Barotrauma public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage); public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def); + public ushort GetAttributeUInt16(string key, ushort def) => Element.GetAttributeUInt16(key, def); public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def); public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def); public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def); diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index e381b89ac..d3b52fe14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -15,20 +17,20 @@ namespace Barotrauma public abstract bool EndsCoroutine(CoroutineHandle handle); } - class EnumCoroutineStatus : CoroutineStatus + sealed class EnumCoroutineStatus : CoroutineStatus { private enum StatusValue { Running, Success, Failure } - private readonly StatusValue Value; + private readonly StatusValue value; - private EnumCoroutineStatus(StatusValue value) { Value = value; } + private EnumCoroutineStatus(StatusValue value) { this.value = value; } - public new readonly static EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running); - public new readonly static EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success); - public new readonly static EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure); + public new static readonly EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running); + public new static readonly EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success); + public new static readonly EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure); public override bool CheckFinished(float deltaTime) { @@ -37,43 +39,74 @@ namespace Barotrauma public override bool EndsCoroutine(CoroutineHandle handle) { - if (Value == StatusValue.Failure) + if (value == StatusValue.Failure) { DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); } - return Value != StatusValue.Running; + return value != StatusValue.Running; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - return obj is EnumCoroutineStatus other && Value == other.Value; + return obj is EnumCoroutineStatus other && value == other.value; } public override int GetHashCode() { - return Value.GetHashCode(); + return value.GetHashCode(); } public override string ToString() { - return Value.ToString(); + return value.ToString(); + } + } + + sealed class WaitForSeconds : CoroutineStatus + { + public readonly float TotalTime; + + private float timer; + private readonly bool ignorePause; + + public WaitForSeconds(float time, bool ignorePause = true) + { + timer = time; + TotalTime = time; + this.ignorePause = ignorePause; + } + + public override bool CheckFinished(float deltaTime) + { +#if !SERVER + if (ignorePause || !CoroutineManager.Paused) + { + timer -= deltaTime; + } +#else + timer -= deltaTime; +#endif + return timer <= 0.0f; + } + + public override bool EndsCoroutine(CoroutineHandle handle) + { + return false; } } - class CoroutineHandle + sealed class CoroutineHandle { public readonly IEnumerator Coroutine; public readonly string Name; - public Exception Exception; - public volatile bool AbortRequested; + public Exception? Exception; + public bool AbortRequested; - public Thread Thread; - - public CoroutineHandle(IEnumerator coroutine, string name = "", bool useSeparateThread = false) + public CoroutineHandle(IEnumerator coroutine, string name = "") { Coroutine = coroutine; - Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name; + Name = string.IsNullOrWhiteSpace(name) ? (coroutine.ToString() ?? "") : name; Exception = null; } @@ -88,7 +121,7 @@ namespace Barotrauma public static bool Paused { get; private set; } - public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "", bool useSeparateThread = false) + public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "") { var handle = new CoroutineHandle(func.GetEnumerator(), name); lock (Coroutines) @@ -96,17 +129,6 @@ namespace Barotrauma Coroutines.Add(handle); } - handle.Thread = null; - if (useSeparateThread) - { - handle.Thread = new Thread(() => { ExecuteCoroutineThread(handle); }) - { - Name = "Coroutine Thread (" + handle.Name + ")", - IsBackground = true - }; - handle.Thread.Start(); - } - return handle; } @@ -115,11 +137,12 @@ namespace Barotrauma return StartCoroutine(DoInvokeAfter(action, delay)); } - private static IEnumerable DoInvokeAfter(Action action, float delay) + private static IEnumerable DoInvokeAfter(Action? action, float delay) { if (action == null) { yield return CoroutineStatus.Failure; + yield break; } if (delay > 0.0f) @@ -169,20 +192,12 @@ namespace Barotrauma private static void HandleCoroutineStopping(Func filter) { + // No lock here because all callers lock for us foreach (CoroutineHandle coroutine in Coroutines) { if (filter(coroutine)) { coroutine.AbortRequested = true; - if (coroutine.Thread != null) - { - bool joined = false; - while (!joined) - { - CrossThread.ProcessTasks(); - joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500)); - } - } } } } @@ -199,49 +214,13 @@ namespace Barotrauma return false; } - public static void ExecuteCoroutineThread(CoroutineHandle handle) - { - try - { - while (!handle.AbortRequested) - { - if (PerformCoroutineStep(handle)) { return; } - Thread.Sleep((int)(DeltaTime * 1000)); - } - } - catch (ThreadAbortException) - { - //not an error, don't worry about it - } - catch (Exception e) - { - handle.Exception = e; - DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has thrown an exception", e); - } - } - private static bool IsDone(CoroutineHandle handle) { #if !DEBUG try { #endif - if (handle.Thread == null) - { - return PerformCoroutineStep(handle); - } - else - { - if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped)) - { - if (handle.Exception != null || handle.Coroutine.Current == CoroutineStatus.Failure) - { - DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); - } - return true; - } - return false; - } + return PerformCoroutineStep(handle); #if !DEBUG } catch (Exception e) @@ -256,27 +235,27 @@ namespace Barotrauma #endif } // Updating just means stepping through all the coroutines + private static readonly List coroutinePass = new List(); public static void Update(bool paused, float deltaTime) { Paused = paused; DeltaTime = deltaTime; - List coroutineList; + // Do not optimize this as a for loop directly over the Coroutines list! + // Coroutines are able to spawn new coroutines! lock (Coroutines) { - coroutineList = Coroutines.ToList(); + coroutinePass.AddRange(Coroutines); } - - foreach (var coroutine in coroutineList) + foreach (var coroutine in coroutinePass) { - if (IsDone(coroutine)) + if (!IsDone(coroutine)) { continue; } + lock (Coroutines) { - lock (Coroutines) - { - Coroutines.Remove(coroutine); - } + Coroutines.Remove(coroutine); } } + coroutinePass.Clear(); } public static void ListCoroutines() @@ -292,37 +271,4 @@ namespace Barotrauma } } } - - class WaitForSeconds : CoroutineStatus - { - public readonly float TotalTime; - - private float timer; - private readonly bool ignorePause; - - public WaitForSeconds(float time, bool ignorePause = true) - { - timer = time; - TotalTime = time; - this.ignorePause = ignorePause; - } - - public override bool CheckFinished(float deltaTime) - { -#if !SERVER - if (ignorePause || !CoroutineManager.Paused) - { - timer -= deltaTime; - } -#else - timer -= deltaTime; -#endif - return timer <= 0.0f; - } - - public override bool EndsCoroutine(CoroutineHandle handle) - { - return false; - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index b7c419430..af66e1865 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -336,7 +336,14 @@ namespace Barotrauma NewMessage("Enemy AI enabled", Color.Green); }, isCheat: true)); - commands.Add(new Command("starttraitormissionimmediately", "starttraitormissionimmediately: Skip the initial delay of the traitor mission and start one immediately.", null)); + commands.Add(new Command("triggertraitorevent|starttraitoreventimmediately", "triggertraitorevent [eventidentifier]: Skip the initial delay of the traitor events and start one immediately. You can optionally specify which event to start (otherwise a random event is chosen).", null, + () => + { + return new string[][] + { + EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Select(p => p.Identifier.ToString()).ToArray() + }; + })); commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null)); @@ -644,7 +651,7 @@ namespace Barotrauma commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) => { if (args.Length == 0) { return; } - foreach (MapEntity mapEntity in MapEntity.mapEntityList) + foreach (MapEntity mapEntity in MapEntity.MapEntityList) { if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)) { @@ -815,8 +822,12 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null && args.Length > 0) { EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]); - - if (eventPrefab != null) + if (eventPrefab is TraitorEventPrefab) + { + ThrowError($"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it."); + return; + } + else if (eventPrefab != null) { var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) @@ -824,8 +835,7 @@ namespace Barotrauma NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); return; } - GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(); + GameMain.GameSession.EventManager.ActivateEvent(newEvent); NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); return; } @@ -844,9 +854,36 @@ 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) + { + var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]); + if (ev == null) + { + ThrowError($"Event \"{args[0]}\" not found."); + } + else + { + string info = ev.GetDebugInfo(); +#if SERVER + //strip rich text tags + RichTextData.GetRichTextData(info, out info); +#endif + NewMessage(info); + } + } + }, isCheat: true, getValidArgs: () => + { + return new[] + { + GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + }; + })); + commands.Add(new Command("unlockmission", "unlockmission [identifier/tag]: Unlocks a mission in a random adjacent level.", (string[] args) => { - if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) + if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { ThrowError("The unlockmission command is only usable in the campaign mode."); return; @@ -1640,7 +1677,7 @@ namespace Barotrauma List pumps = new List(); foreach (Item item in Submarine.MainSub.GetItems(true)) { - if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent() is { } pump) + if (item.CurrentHull != null && item.HasTag(Tags.Ballast) && item.GetComponent() is { } pump) { if (item.CurrentHull.BallastFlora != null) { continue; } pumps.Add(pump); @@ -2224,8 +2261,8 @@ namespace Barotrauma string itemNameOrId = args[0].ToLowerInvariant(); ItemPrefab itemPrefab = - (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + (MapEntityPrefab.FindByName(itemNameOrId) ?? + MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab; if (itemPrefab == null) { errorMsg = "Item \"" + itemNameOrId + "\" not found!"; @@ -2320,6 +2357,19 @@ namespace Barotrauma } } + /// + /// Throws the error in debug builds. In non-debug builds, logs it instead. + /// Use for handling non-critical errors that shouldn't go unnoticed in debug builds (like warnings might), but which don't break the game and thus doesn't have to open the console. + /// + public static void AddSafeError(string error) + { +#if DEBUG + DebugConsole.ThrowError(error); +#else + DebugConsole.LogError(error); +#endif + } + public static void LogError(string msg, Color? color = null) { color ??= Color.Red; @@ -2493,7 +2543,16 @@ namespace Barotrauma LogError(error); } - + + public static void ThrowErrorAndLogToGA(string gaIdentifier, string errorMsg) + { + ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + gaIdentifier, + GameAnalyticsManager.ErrorSeverity.Error, + errorMsg); + } + public static void AddWarning(string warning) { System.Diagnostics.Debug.WriteLine(warning); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index dcd1deeea..16622dee8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -64,7 +64,16 @@ namespace Barotrauma spawnPending = true; } - + + public override string GetDebugInfo() + { + return + $"Finished: {IsFinished.ColorizeObject()}\n" + + $"Item: {Item.ColorizeObject()}\n" + + $"Spawn pending: {SpawnPending.ColorizeObject()}\n" + + $"Spawn position: {SpawnPos.ColorizeObject()}"; + } + private void SpawnItem() { item = new Item(itemPrefab, spawnPos, null); @@ -73,7 +82,7 @@ namespace Barotrauma //try to find an artifact holder and place the artifact inside it foreach (Item it in Item.ItemList) { - if (it.Submarine != null || !it.HasTag("artifactholder")) continue; + if (it.Submarine != null || !it.HasTag(Tags.ArtifactHolder)) { continue; } var itemContainer = it.GetComponent(); if (itemContainer == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 1b41e3bf6..1df74bcb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -52,6 +52,11 @@ namespace Barotrauma ParentSet = parentSet; } + public virtual string GetDebugInfo() + { + return $"Finished: {IsFinished.ColorizeObject()}"; + } + public virtual void Update(float deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index ca7fcad9d..fb75303ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -12,6 +12,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } = Identifier.Empty; + [Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")] + public Identifier SourceCharacter { get; set; } = Identifier.Empty; + [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")] public LimbType TargetLimb { get; set; } @@ -33,8 +36,7 @@ namespace Barotrauma if (target.CharacterHealth == null) { continue; } if (TargetLimb == LimbType.None) { - var affliction = target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions); - if (affliction != null && affliction.Strength >= MinStrength) { return true; } + if (target.CharacterHealth.GetAfflictionStrengthByIdentifier(Identifier, AllowLimbAfflictions) >= MinStrength) { return true; } } IEnumerable afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction => { @@ -43,9 +45,13 @@ namespace Barotrauma LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; if (limbType == null || limbType != TargetLimb) { return false; } } + if (!SourceCharacter.IsEmpty) + { + if (!ParentEvent.GetTargets(SourceCharacter).Contains(affliction.Source)) { return false; } + } + return affliction.Strength >= MinStrength; }); - if (afflictions.Any(a => a.Identifier == Identifier)) { return true; } } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index af4f0c713..5fed3e0ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -46,7 +46,7 @@ namespace Barotrauma } if (target == null) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); + DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); } if (target == null || Conditional == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index bd30a9b4c..df038e70c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -85,7 +85,7 @@ namespace Barotrauma case bool bool1 when metadata2 is bool bool2: return CompareBool(bool1, bool2) ?? false; case float float1 when metadata2 is float float2: - return CompareFloat(float1, float2) ?? false; + return PropertyConditional.CompareFloat(float1, float2, Operator); } } @@ -143,36 +143,13 @@ namespace Barotrauma { if (float.TryParse(value, out float f)) { - return CompareFloat(GetFloat(campaignMode), f); + return PropertyConditional.CompareFloat(GetFloat(campaignMode), f, Operator); } DebugConsole.Log($"{value} != float"); return null; } - private bool? CompareFloat(float val1, float val2) - { - value1 = val1; - value2 = val2; - switch (Operator) - { - case PropertyConditional.ComparisonOperatorType.Equals: - return MathUtils.NearlyEqual(val1, val2); - case PropertyConditional.ComparisonOperatorType.GreaterThan: - return val1 > val2; - case PropertyConditional.ComparisonOperatorType.GreaterThanEquals: - return val1 >= val2; - case PropertyConditional.ComparisonOperatorType.LessThan: - return val1 < val2; - case PropertyConditional.ComparisonOperatorType.LessThanEquals: - return val1 <= val2; - case PropertyConditional.ComparisonOperatorType.NotEquals: - return !MathUtils.NearlyEqual(val1, val2); - } - - return null; - } - private bool? TryString(CampaignMode campaignMode, string value) { return CompareString(GetString(campaignMode), value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index d202db5b8..4d53e6eca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,8 +1,8 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -20,9 +20,15 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] + public Identifier HullTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first target when the check succeeds.")] public Identifier ApplyTagToTarget { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")] + public Identifier ApplyTagToItem { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool RequireEquipped { get; set; } @@ -32,6 +38,24 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes)] public int ItemContainerIndex { get; set; } + private readonly bool checkPercentage; + + private float requiredConditionalMatchPercentage; + + [Serialize(100.0f, IsPropertySaveable.Yes)] + + /// + /// What percentage of targets do the conditionals need to match for the check to succeed? + /// + public float RequiredConditionalMatchPercentage + { + get { return requiredConditionalMatchPercentage; } + set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); } + } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool CompareToInitialAmount { get; set; } + private readonly IReadOnlyList conditionals; private readonly Identifier[] itemIdentifierSplit; @@ -49,16 +73,84 @@ namespace Barotrauma } conditionals = conditionalList; - if (itemTags.None() && ItemIdentifiers.None()) + if (itemTags.None() && + ItemIdentifiers.None() && + TargetTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check."); } + + checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null; + if (Amount != 1 && checkPercentage) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}."); + } } + private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched) + { + if (CompareToInitialAmount) + { + totalTargets = ParentEvent.GetInitialTargetCount(TargetTag); + } + if (checkPercentage) + { + return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage; + } + else + { + return targetsWithConditionalsMatched >= Amount; + } + } + + private readonly List tempTargetItems = new List(); protected override bool? DetermineSuccess() { var targets = ParentEvent.GetTargets(TargetTag); - if (!targets.Any()) { return null; } + + if (!HullTag.IsEmpty) + { + var hulls = ParentEvent.GetTargets(HullTag).OfType(); + targets = targets.Where(t => + (t is Item it && hulls.Contains(it.CurrentHull)) || + (t is Character c && hulls.Contains(c.CurrentHull))); + } + + if (!targets.Any()) + { + if (conditionals.Any()) + { + //conditionals can't be met if there's no targets + return false; + } + return null; + } + + //check if the target(s) are the items we're looking for (instead of characters/containers the items are inside) + int targetCount = targets.Count(); + if (targetCount >= Amount) + { + tempTargetItems.Clear(); + foreach (var target in targets) + { + if (target is not Item item) { continue; } + if (itemTags.Any() && itemTags.None(item.HasTag) && + itemIdentifierSplit.Any() && !itemIdentifierSplit.Contains(item.Prefab.Identifier)) + { + continue; + } + if (ConditionalsMatch(item, character: null)) + { + tempTargetItems.Add(item); + } + } + if (EnoughTargets(targetCount, tempTargetItems.Count)) + { + TryApplyTagToItems(tempTargetItems); + return true; + } + } + foreach (var target in targets) { if (target is Character character) @@ -99,8 +191,9 @@ namespace Barotrauma private bool CheckInventory(Inventory inventory, Character character) { if (inventory == null) { return false; } - int count = 0; + int targetCount = 0; HashSet eventTargets = new HashSet(); + tempTargetItems.Clear(); foreach (Identifier tag in itemTags) { foreach (var target in ParentEvent.GetTargets(tag)) @@ -117,19 +210,39 @@ namespace Barotrauma eventTargets.Contains(it), recursive: Recursive)) { - if (!ConditionalsMatch(item, character)) { continue; } - count++; - if (count >= Amount) { return true; } + targetCount++; + if (ConditionalsMatch(item, character)) + { + tempTargetItems.Add(item); + } + } + + if (EnoughTargets(targetCount, tempTargetItems.Count)) + { + TryApplyTagToItems(tempTargetItems); + return true; } return false; + + } + + private void TryApplyTagToItems(IEnumerable items) + { + if (!ApplyTagToItem.IsEmpty) + { + foreach (var targetItem in items) + { + ParentEvent.AddTarget(ApplyTagToItem, targetItem); + } + } } private bool ConditionalsMatch(Item item, Character character = null) { if (item == null) { return false; } foreach (PropertyConditional conditional in conditionals) - { - if (!conditional.Matches(item)) + { + if (!item.ConditionalMatches(conditional)) { return false; } @@ -145,8 +258,8 @@ namespace Barotrauma public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + - $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" + - $"Succeeded: {succeeded.ColorizeObject()})"; + (ItemTags.Any() ? $"ItemTags: {ItemTags.ColorizeObject()}, " : $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}, ") + + $"Succeeded: {succeeded.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs new file mode 100644 index 000000000..f3833fa6d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs @@ -0,0 +1,35 @@ +#nullable enable + +namespace Barotrauma +{ + class CheckTraitorEventStateAction : BinaryOptionAction + { + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + public TraitorEvent.State State { get; set; } + + private readonly TraitorEvent? traitorEvent; + + public CheckTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (parentEvent is TraitorEvent traitorEvent) + { + this.traitorEvent = traitorEvent; + } + else + { + DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + } + } + + protected override bool? DetermineSuccess() + { + return traitorEvent?.CurrentState == State; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckTraitorEventStateAction)} -> " + + $"State: {State.ColorizeObject()}, Succeeded: {succeeded.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs new file mode 100644 index 000000000..7f8e26526 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs @@ -0,0 +1,40 @@ +#nullable enable +using Barotrauma.Networking; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Checks whether the specific target was voted as the traitor. + /// + class CheckTraitorVoteAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Target { get; set; } + + public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (parentEvent is not TraitorEvent) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events."); + } + } + + protected override bool? DetermineSuccess() + { + var targetEntities = ParentEvent.GetTargets(Target); +#if SERVER + if (GameMain.Server?.TraitorManager?.GetClientAccusedAsTraitor() is Client traitorClient) + { + return targetEntities.Any(e => e is Character character && traitorClient?.Character == character); + } +#endif + return false; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckTraitorVoteAction)} -> (TargetTag: {Target.ColorizeObject()}"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs new file mode 100644 index 000000000..35e4eeb9c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -0,0 +1,60 @@ +#nullable enable + + +namespace Barotrauma +{ + class CheckVisibilityAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")] + public Identifier EntityTag { 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("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")] + public Identifier ApplyTagToEntity { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity that was seen when the check succeeds.")] + public Identifier ApplyTagToTarget { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "If both the seeing entity and the target are the same, does it count as success?")] + public bool AllowSameEntity { get; set; } + + public CheckVisibilityAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + protected override bool? DetermineSuccess() + { + foreach (var entity in ParentEvent.GetTargets(EntityTag)) + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (!AllowSameEntity && entity == target) { continue; } + if (Character.IsTargetVisible(target, entity, CheckFacing)) + { + if (!ApplyTagToEntity.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToEntity, entity); + } + if (!ApplyTagToTarget.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToTarget, target); + } + return true; + } + } + } + + return false; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckVisibilityAction)} -> (TargetTags: {EntityTag.ColorizeObject()}, {TargetTag.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index 26320d4f9..9f1242e13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -43,13 +43,14 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { - if (!(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.Removed) { continue; } + if (npc.AIController is not HumanAIController humanAiController) { continue; } Character enemy = null; float closestDist = float.MaxValue; foreach (Entity target in ParentEvent.GetTargets(EnemyTag)) { - if (!(target is Character character)) { continue; } + if (target is not Character character) { continue; } float dist = Vector2.DistanceSquared(npc.WorldPosition, target.WorldPosition); if (dist < closestDist) { @@ -82,7 +83,7 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; } foreach (var combatObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) { combatObjective.Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index a01b1abd4..8092bdb91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -59,6 +59,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")] + public bool ContinueAutomatically { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool IgnoreInterruptDistance { get; set; } @@ -86,6 +89,8 @@ namespace Barotrauma private bool interrupt; + private readonly XElement textElement; + public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { actionCount++; @@ -93,15 +98,41 @@ namespace Barotrauma Options = new List(); foreach (var elem in element.Elements()) { - if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase)) + if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase)) { Options.Add(new SubactionGroup(ParentEvent, elem)); } - else if (elem.Name.LocalName.Equals("interrupt", StringComparison.InvariantCultureIgnoreCase)) + else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase)) { Interrupted = new SubactionGroup(ParentEvent, elem); } + else if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase)) + { + Text = elem.GetAttributeString("tag", string.Empty); + textElement = elem; + } } + if (element.GetChildElement("Replace") != null) + { + DebugConsole.ThrowError( + $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + + $" - unrecognized child element \"Replace\"."); + } + } + + public LocalizedString GetDisplayText() + { + LocalizedString text = string.Empty; + + if (textElement != null) + { + TextManager.ConstructDescription(ref text, textElement, ParentEvent.GetTextForReplacementElement); + } + else + { + text = TextManager.Get(Text).Fallback(Text); + } + return ParentEvent.ReplaceVariablesInEventText(text); } public override IEnumerable GetSubActions() @@ -145,9 +176,14 @@ namespace Barotrauma } } + if (ContinueAutomatically && Options.None()) + { + return dialogOpened; + } + if (selectedOption >= 0) { - if (!Options.Any() || Options[selectedOption].IsFinished(ref goTo)) + if (Options.None() || Options[selectedOption].IsFinished(ref goTo)) { ResetSpeaker(); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs new file mode 100644 index 000000000..c2287af04 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -0,0 +1,120 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class CountTargetsAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Optional second tag. Can be used if the target must have two different tags.")] + public Identifier SecondRequiredTargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] + public Identifier HullTag { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int MinAmount { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int MaxAmount { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CompareToTarget { get; set; } + + [Serialize(-1.0f, IsPropertySaveable.Yes)] + + /// + /// Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget + /// E.g. you could compare the number of entities tagged as "discoveredhull" to entities tagged as "anyhull" to require 50% of hulls to be discovered. + /// + public float MinPercentageRelativeToTarget { get; set; } + + [Serialize(-1.0f, IsPropertySaveable.Yes)] + + /// + /// Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget + /// E.g. you could compare the number of entities tagged as "floodedhull" to entities tagged as "anyhull" to require less than 50% of hulls to be flooded. + /// + public float MaxPercentageRelativeToTarget { get; set; } + + private readonly IReadOnlyList conditionals; + + public CountTargetsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + var conditionalList = new List(); + foreach (ContentXElement subElement in element.GetChildElements("conditional")) + { + conditionalList.AddRange(PropertyConditional.FromXElement(subElement!)); + } + conditionals = conditionalList; + + if (CompareToTarget.IsEmpty) + { + int amount = element.GetAttributeInt("amount", -1); + if (amount > -1) + { + MinAmount = MaxAmount = amount; + } + if (MinAmount > MaxAmount && MaxAmount > -1) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}."); + } + } + else + { + if (MinPercentageRelativeToTarget < 0.0f && MaxPercentageRelativeToTarget < 0.0f) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set."); + } + } + } + + protected override bool? DetermineSuccess() + { + var potentialTargets = ParentEvent.GetTargets(TargetTag); + + if (!SecondRequiredTargetTag.IsEmpty) + { + potentialTargets = potentialTargets.Where(t => ParentEvent.GetTargets(SecondRequiredTargetTag).Contains(t)); + } + if (!HullTag.IsEmpty) + { + var hulls = ParentEvent.GetTargets(HullTag).OfType(); + potentialTargets = potentialTargets.Where(t => + (t is Item it && hulls.Contains(it.CurrentHull)) || + (t is Character c && hulls.Contains(c.CurrentHull))); + } + + if (conditionals.Any()) + { + potentialTargets = potentialTargets.Where(t => conditionals.Any(c => c.Matches(t as ISerializableEntity))); + } + + int targetCount = potentialTargets.Count(); + + if (CompareToTarget.IsEmpty) + { + if (MinAmount > -1 && targetCount < MinAmount) { return false; } + if (MaxAmount > -1 && targetCount > MaxAmount) { return false; } + } + else + { + int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count(); + float percentage = MathUtils.Percentage(targetCount, compareToTargetCount); + if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; } + if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; } + } + return true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CountTargetsAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"Succeeded: {succeeded.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index b570a750d..dd086ec74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma { @@ -138,12 +137,17 @@ namespace Barotrauma Type actionType; try { - actionType = Type.GetType("Barotrauma." + element.Name, true, true); + Identifier typeName = element.Name.ToString().ToIdentifier(); + if (typeName == "TutorialSegmentAction") + { + typeName = "EventObjectiveAction".ToIdentifier(); + } + actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true); if (actionType == null) { throw new NullReferenceException(); } } catch { - DebugConsole.ThrowError("Could not find an event class of the type \"" + element.Name + "\"."); + DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\"."); return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs new file mode 100644 index 000000000..7fc3af8b5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs @@ -0,0 +1,94 @@ +#nullable enable + +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class EventLogAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Id { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Text { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + public bool ShowInServerLog { get; set; } + + private readonly XElement? textElement; + + public EventLogAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Id == Identifier.Empty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id."); + } + //append the target tag so logs targeted to different players don't interfere with each other even if they use the same Id + Id = (Id.ToString() + TargetTag).ToIdentifier(); + + foreach (var elem in element.Elements()) + { + if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase)) + { + textElement = elem; + break; + } + } + Text ??= string.Empty; + if (textElement == null) + { + if (Text.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element})."); + } + else + { + Text = TextManager.Get(Text).Fallback(Text).Value; + } + } + ShowInServerLog = element.GetAttributeBool(nameof(ShowInServerLog), ParentEvent is TraitorEvent); + } + + private bool isFinished; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + + public LocalizedString GetDisplayText() + { + LocalizedString text = Text; + if (textElement != null) + { + LocalizedString tempDescription = string.Empty; + TextManager.ConstructDescription(ref tempDescription, textElement, ParentEvent.GetTextForReplacementElement); + text = tempDescription.Value; + } + return ParentEvent.ReplaceVariablesInEventText(text); + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + AddEntryProjSpecific(GameMain.GameSession?.EventManager?.EventLog, GetDisplayText().Value); + isFinished = true; + } + + partial void AddEntryProjSpecific(EventLog? eventLog, string displayText); + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(EventLogAction)} -> (Id: {Id})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs similarity index 62% rename from Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs rename to Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index f2128f82e..17316466f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -1,8 +1,8 @@ namespace Barotrauma { - partial class TutorialSegmentAction : EventAction + partial class EventObjectiveAction : EventAction { - public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove }; + public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] public SegmentActionType Type { get; set; } @@ -34,14 +34,29 @@ namespace Barotrauma [Serialize(80, IsPropertySaveable.Yes)] public int Height { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + private bool isFinished; - public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + public EventObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (Identifier.IsEmpty) { Identifier = element.GetAttributeIdentifier("id", Identifier.Empty); } + if (Type != SegmentActionType.Trigger && !TextTag.IsEmpty) + { + DebugConsole.ThrowError( + $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\""+ + $" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video."); + } + if (element.GetChildElement("Replace") != null) + { + DebugConsole.ThrowError( + $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + + $" - unrecognized child element \"Replace\"."); + } } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs new file mode 100644 index 000000000..7031d0daf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs @@ -0,0 +1,49 @@ +using System.Linq; + +namespace Barotrauma +{ + class GiveExpAction : EventAction + { + [Serialize(0, IsPropertySaveable.Yes)] + public int Amount { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + public GiveExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (TargetTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check)."); + } + } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); + foreach (var target in targets) + { + target.Info?.GiveExperience(Amount); + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(GiveExpAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"Amount: {Amount.ColorizeObject()})"; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 12290178a..b051966cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -1,6 +1,4 @@ -using Microsoft.Xna.Framework; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -19,7 +17,7 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": GiveSkillExpAction without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check)."); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index 6f186b127..95c317203 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -1,5 +1,3 @@ -using System.Xml.Linq; - namespace Barotrauma { class GoTo : EventAction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index e86414d96..64861f6e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -100,7 +100,7 @@ namespace Barotrauma WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human); if (subWaypoint != null) { - npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); + npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 6546199a0..94111f0fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -25,8 +25,8 @@ namespace Barotrauma public NPCFollowAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - private List affectedNpcs = null; - private Entity target = null; + private IEnumerable affectedNpcs; + private Entity target; public override void Update(float deltaTime) { @@ -36,9 +36,10 @@ namespace Barotrauma if (target == null) { return; } int targetCount = 0; - affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); + affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character); foreach (var npc in affectedNpcs) { + if (npc.Removed) { continue; } if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Follow) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs index b39eb465f..e3e8c9ade 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -48,6 +48,7 @@ namespace Barotrauma affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { + if (npc.Removed) { continue; } if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Operate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 3a6f61fe5..28db501f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -27,6 +27,7 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { + if (npc.Removed) { continue; } if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Wait) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs new file mode 100644 index 000000000..23bb3b63a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs @@ -0,0 +1,42 @@ +#nullable enable + +namespace Barotrauma +{ + class OnRoundEndAction : EventAction + { + private readonly SubactionGroup subActions; + + public OnRoundEndAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + subActions = new SubactionGroup(parentEvent, element); + } + + public override bool IsFinished(ref string goToLabel) + { + return false; + } + + public override void Update(float deltaTime) + { + int remainingTries = 100; + string? throwaway = null; + //normally the ref string goTo passed to IsFinished should be used to jump another place in the event, + //but in this case we don't want that (the subactions should just run once when the round ends) + while (remainingTries > 0 && !subActions.IsFinished(ref throwaway)) + { + subActions.Update(deltaTime); + Entity.Spawner?.Update(createNetworkEvents: false); + remainingTries--; + } + } + + public override void Reset() + { + } + + public override string ToDebugString() + { + return nameof(OnRoundEndAction); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs new file mode 100644 index 000000000..5361d5696 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs @@ -0,0 +1,48 @@ +#nullable enable + +namespace Barotrauma +{ + class SetTraitorEventStateAction : EventAction + { + private readonly TraitorEvent? traitorEvent; + + public SetTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (parentEvent is TraitorEvent traitorEvent) + { + this.traitorEvent = traitorEvent; + } + else + { + DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + } + } + + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + public TraitorEvent.State State { get; set; } + + private bool isFinished; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished || traitorEvent == null) { return; } + traitorEvent.CurrentState = State; + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetTraitorEventStateAction)} -> (State: {State})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index f015fc26b..e9c3a624e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -16,7 +16,8 @@ namespace Barotrauma MainPath, Ruin, Wreck, - BeaconStation + BeaconStation, + NearMainSub } [Serialize("", IsPropertySaveable.Yes, description: "Species name of the character to spawn.")] @@ -60,6 +61,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")] public bool AllowDuplicates { get; set; } + [Serialize(1, IsPropertySaveable.Yes)] + public int Amount { get; set; } + [Serialize(100.0f, IsPropertySaveable.Yes)] public float Offset { get; set; } @@ -84,6 +88,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] public bool IgnoreByAI { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")] + public bool AllowInPlayerView { get; set; } + private bool spawned; private Entity spawnedEntity; @@ -159,40 +166,43 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => + for (int i = 0; i < Amount; i++) { - if (newCharacter == null) { return; } - newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = TeamID; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); - if (LootingIsStealing) + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) + if (newCharacter == null) { return; } + newCharacter.HumanPrefab = humanPrefab; + newCharacter.TeamID = TeamID; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); + if (LootingIsStealing) { - item.SpawnedInCurrentOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) + { + item.SpawnedInCurrentOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); - if (!TargetTag.IsEmpty && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) - { - outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); - foreach (Identifier tag in humanPrefab.GetTags()) + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!TargetTag.IsEmpty && newCharacter != null) { - outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + } } - } #if SERVER - newCharacter.LoadTalents(); - GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); + newCharacter.LoadTalents(); + GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); #endif - }); + }); + } } } } @@ -206,14 +216,17 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => + for (int i = 0; i < Amount; i++) { - if (!TargetTag.IsEmpty && newCharacter != null) + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); + if (!TargetTag.IsEmpty && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } } } else if (!ItemIdentifier.IsEmpty) @@ -243,7 +256,7 @@ namespace Barotrauma if (spawnInventory == null) { - DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\""); + DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found."); } } @@ -252,12 +265,19 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); + for (int i = 0; i < Amount; i++) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); + } } } else { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); + for (int i = 0; i < Amount; i++) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); + + } } void onSpawned(Item newItem) { @@ -298,18 +318,27 @@ namespace Barotrauma { if (!SpawnPointTag.IsEmpty) { - List potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); + IEnumerable potentialItems = Item.ItemList.Where(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); + if (!AllowInPlayerView) + { + potentialItems = GetEntitiesNotInPlayerView(potentialItems); + } var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced(); + var potentialTargets = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)); + if (!AllowInPlayerView) + { + potentialTargets = GetEntitiesNotInPlayerView(potentialTargets); + } + var target = potentialTargets.GetRandomUnsynced(); if (target != null) { return target; } } SpawnType? spawnPointType = null; if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; } - return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); + return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView); } private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine) @@ -318,16 +347,39 @@ namespace Barotrauma { SpawnLocationType.Any => true, SpawnLocationType.MainSub => submarine == Submarine.MainSub, + SpawnLocationType.NearMainSub => submarine == null, SpawnLocationType.MainPath => submarine == null, - SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } }, - SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } }, - SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } }, + SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true }, + SpawnLocationType.Wreck => submarine is { Info.IsWreck: true }, + SpawnLocationType.Ruin => submarine is { Info.IsRuin: true }, SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null, _ => throw new NotImplementedException(), }; } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) + /// + /// Returns those of the entities that aren't in any player's view. If there are none, all the entities are returned. + /// + private static IEnumerable GetEntitiesNotInPlayerView(IEnumerable entities) where T : ISpatialEntity + { + if (entities.Any(e => !IsInPlayerView(e))) + { + return entities.Where(e => !IsInPlayerView(e)); + } + return entities; + } + + private static bool IsInPlayerView(ISpatialEntity entity) + { + foreach (var character in Character.CharacterList) + { + if (!character.IsPlayer || character.IsDead) { continue; } + if (character.CanSeeTarget(entity)) { return true; } + } + return false; + } + + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false, bool allowInPlayerView = true) { bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); @@ -385,6 +437,12 @@ namespace Barotrauma return potentialSpawnPoints.GetRandomUnsynced(); } + if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub) + { + validSpawnPoints = validSpawnPoints.Where(p => + Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition))); + } + //avoid using waypoints if there's any actual spawnpoints available if (validSpawnPoints.Any(wp => wp.SpawnType != SpawnType.Path)) { @@ -401,7 +459,27 @@ namespace Barotrauma } } - if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any()) + if (!allowInPlayerView) + { + validSpawnPoints = GetEntitiesNotInPlayerView(validSpawnPoints); + } + + if (spawnLocation == SpawnLocationType.NearMainSub && Submarine.MainSub != null) + { + WayPoint closestPoint = validSpawnPoints.First(); + float closestDist = float.PositiveInfinity; + foreach (WayPoint wp in validSpawnPoints) + { + float dist = Vector2.DistanceSquared(wp.WorldPosition, Submarine.MainSub.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + closestPoint = wp; + } + } + return closestPoint; + } + else if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any()) { WayPoint furthestPoint = validSpawnPoints.First(); float furthestDist = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index ef66959e5..bb17f02c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -24,14 +25,25 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool AllowHiddenItems { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool ChooseRandom { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "If larger than 0, the specified percentage of the matching targets are tagged. Between 0-100.")] + public float ChoosePercentage { get; set; } + private bool isFinished = false; + private bool targetNotFound = false; + public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { Taggers = new (string k, Action v)[] { ("players", v => TagPlayers()), ("player", v => TagPlayers()), + ("traitor", v => TagTraitors()), + ("nontraitor", v => TagNonTraitors()), + ("nontraitorplayer", v => TagNonTraitorPlayers()), ("bot", v => TagBots(playerCrewOnly: false)), ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), @@ -40,8 +52,10 @@ namespace Barotrauma ("structurespecialtag", TagStructuresBySpecialTag), ("itemidentifier", TagItemsByIdentifier), ("itemtag", TagItemsByTag), + ("hull", v => TagHulls()), ("hullname", TagHullsByName), ("submarine", TagSubmarinesByType), + ("eventtag", TagByEventTag), }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); } @@ -54,34 +68,44 @@ namespace Barotrauma isFinished = false; } + private void TagByEventTag(Identifier eventTag) + { + AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => SubmarineTypeMatches(t.Submarine))); + } + private void TagPlayers() { - if (IgnoreIncapacitatedCharacters) - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && !c.IsIncapacitated); - } - else - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer); - } + AddTargetPredicate(Tag, 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); + } + + private void TagNonTraitors() + { + AddTargetPredicate(Tags.NonTraitor, 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); } private void TagBots(bool playerCrewOnly) { - if (IgnoreIncapacitatedCharacters) - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); - } - else - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); - } + AddTargetPredicate(Tag, e => + e is Character c && + c.IsBot && + (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && + (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); } private void TagCrew() { #if CLIENT - GameMain.GameSession.CrewManager.GetCharacters().ForEach(c => ParentEvent.AddTarget(Tag, c)); + AddTarget(Tag, GameMain.GameSession.CrewManager.GetCharacters()); #else TagPlayers(); TagBots(playerCrewOnly: true); @@ -90,54 +114,47 @@ namespace Barotrauma private void TagHumansByIdentifier(Identifier identifier) { - foreach (Character c in Character.CharacterList) - { - if (c.HumanPrefab?.Identifier == identifier) - { - ParentEvent.AddTarget(Tag, c); - } - } + AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier)); } private void TagHumansByJobIdentifier(Identifier jobIdentifier) { - foreach (Character c in Character.CharacterList) - { - if (c.HasJob(jobIdentifier)) - { - ParentEvent.AddTarget(Tag, c); - } - } + AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier))); } private void TagStructuresByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); + AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } private void TagStructuresBySpecialTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); } private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); + AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + } + + private void TagHulls() + { + AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine)); } private void TagHullsByName(Identifier name) { - ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); + AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private void TagSubmarinesByType(Identifier type) { - ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } private bool IsValidItem(Item it) @@ -152,7 +169,7 @@ namespace Barotrauma switch (sub.Info.Type) { case Barotrauma.SubmarineType.Player: - return SubmarineType.HasFlag(SubType.Player); + return SubmarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; case Barotrauma.SubmarineType.Outpost: case Barotrauma.SubmarineType.OutpostModule: return SubmarineType.HasFlag(SubType.Outpost); @@ -165,14 +182,86 @@ namespace Barotrauma } } + private void AddTargetPredicate(Identifier tag, Predicate predicate) + { + if (ChoosePercentage > 0.0f) + { + TagPercentage(tag, Entity.GetEntities().Where(e => predicate(e))); + } + else if (ChooseRandom) + { + TagRandom(tag, Entity.GetEntities().Where(e => predicate(e))); + } + else + { + ParentEvent.AddTargetPredicate(tag, predicate); + } + } + + private void AddTarget(Identifier tag, IEnumerable entities) + { + if (entities.None()) + { + targetNotFound = true; + return; + } + if (ChoosePercentage > 0.0f) + { + TagPercentage(tag, entities); + } + else if (ChooseRandom) + { + TagRandom(tag, entities); + } + else + { + foreach (var entity in entities) + { + ParentEvent.AddTarget(tag, entity); + } + } + } + + private List tempEntities; + private void TagPercentage(Identifier tag, IEnumerable entities) + { + if (entities.None()) + { + targetNotFound = true; + return; + } + + int amountToChoose = (int)Math.Ceiling(entities.Count() * (ChoosePercentage / 100.0f)); + + tempEntities ??= new List(); + tempEntities.Clear(); + for (int i = 0; i < amountToChoose; i++) + { + var entity = entities.GetRandomUnsynced(); + tempEntities.Remove(entity); + ParentEvent.AddTarget(tag, entity); + } + } + + private void TagRandom(Identifier tag, IEnumerable entities) + { + if (entities.None()) + { + targetNotFound = true; + return; + } + ParentEvent.AddTarget(tag, entities.GetRandomUnsynced()); + } + private readonly ImmutableDictionary> Taggers; public override void Update(float deltaTime) { - if (isFinished) { return; } + if (isFinished || targetNotFound) { return; } string[] criteriaSplit = Criteria.Split(';'); + targetNotFound = false; foreach (string entry in criteriaSplit) { string[] kvp = entry.Split(':'); @@ -190,7 +279,7 @@ namespace Barotrauma } } - isFinished = true; + isFinished = !targetNotFound; } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 8f7a849ac..5c8bce3d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -2,7 +2,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -50,6 +49,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")] public bool CheckAllTargets { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If true, interacting with the target will make the character select it.")] + public bool SelectOnTrigger { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -186,7 +188,12 @@ namespace Barotrauma { if (npc != null) { - if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) + if (npc.CampaignInteractionType == CampaignMode.InteractionType.Talk) + { + //if the NPC has a conversation available, don't assign the trigger until the conversation is done + continue; + } + else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) { if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc)) { @@ -213,7 +220,8 @@ namespace Barotrauma { npcsOrItems.Add(item); } - item.CampaignInteractionType = CampaignMode.InteractionType.Examine; + item.AssignCampaignInteractionType(CampaignMode.InteractionType.Examine, + GameMain.NetworkMember?.ConnectedClients.Where(c => c.Character != null && targets2.Contains(c.Character))); if (player.SelectedItem == item || player.SelectedSecondaryItem == item || (player.Inventory != null && player.Inventory.Contains(item)) || @@ -276,7 +284,7 @@ namespace Barotrauma } else if (npcOrItem.TryGet(out Item item)) { - item.CampaignInteractionType = CampaignMode.InteractionType.None; + item.AssignCampaignInteractionType(CampaignMode.InteractionType.None); } } } @@ -352,6 +360,37 @@ namespace Barotrauma ParentEvent.AddTarget(ApplyToTarget2, entity2); } + Character player = null; + Entity target = null; + if (entity1 is Character { IsPlayer: true }) + { + player = entity1 as Character; + target = entity2; + } + else if (entity2 is Character { IsPlayer: true }) + { + player = entity2 as Character; + target = entity1; + } + if (player != null && SelectOnTrigger) + { + if (target is Character targetCharacter) + { + player.SelectCharacter(targetCharacter); + } + else if (target is Item targetItem) + { + if (targetItem.IsSecondaryItem) + { + player.SelectedSecondaryItem = targetItem; + } + else + { + player.SelectedItem = targetItem; + } + } + } + isRunning = false; isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs index eebac860d..190f7fdc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs @@ -10,7 +10,13 @@ partial class TutorialHighlightAction : EventAction private bool isFinished; - public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (GameMain.NetworkMember != null) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer."); + } + } public override void Update(float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs new file mode 100644 index 000000000..601981cb6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs @@ -0,0 +1,75 @@ +#nullable enable + +using Barotrauma.Items.Components; +using System.Linq; + +namespace Barotrauma +{ + class WaitForItemFabricatedAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CharacterTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int Amount { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the fabricated item(s).")] + public Identifier ApplyTagToItem { get; set; } + + private int counter; + + public WaitForItemFabricatedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (ItemTag.IsEmpty && ItemIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check."); + } + foreach (var item in Item.ItemList) + { + var fabricator = item.GetComponent(); + if (fabricator != null) + { + fabricator.OnItemFabricated += OnItemFabricated; + } + } + } + + public void OnItemFabricated(Item item, Character character) + { + if (item == null) { return; } + if (!CharacterTag.IsEmpty) + { + if (!ParentEvent.GetTargets(CharacterTag).Contains(character)) { return; } + } + if (item.ContainerIdentifier == ItemTag || item.HasTag(ItemTag)) + { + if (!ApplyTagToItem.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToItem, item); + } + counter++; + } + } + + public override bool IsFinished(ref string goTo) + { + return counter >= Amount; + } + + public override void Reset() + { + counter = 0; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(counter >= Amount)} {nameof(WaitForItemFabricatedAction)} -> ({ItemTag}, {counter}/{Amount})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs new file mode 100644 index 000000000..d7e48a299 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -0,0 +1,153 @@ +#nullable enable + +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class WaitForItemUsedAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier UserTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetItemComponent { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target item when it's used.")] + public Identifier ApplyTagToItem { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the user when the target item is used.")] + public Identifier ApplyTagToUser{ 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; } + + [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; } + + private bool isFinished; + + private readonly HashSet targets = new HashSet(); + private readonly HashSet targetComponents = new HashSet(); + + private Identifier onUseEventIdentifier; + private Identifier OnUseEventIdentifier + { + get + { + if (onUseEventIdentifier.IsEmpty) + { + onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString()).ToIdentifier(); + } + return onUseEventIdentifier; + } + } + + public WaitForItemUsedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (ItemTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(ItemTag)} not set in {nameof(WaitForItemUsedAction)}."); + } + } + + private void OnItemUsed(Item item, Character user) + { + if (!ApplyTagToItem.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToItem, item); + } + if (!ApplyTagToUser.IsEmpty && user != null) + { + 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); + } + } + } + + DeregisterTargets(); + isFinished = true; + } + + public override void Update(float deltaTime) + { + TryRegisterTargets(); + } + + private void TryRegisterTargets() + { + foreach (Entity target in ParentEvent.GetTargets(ItemTag)) + { + //already registered, ignore + if (targets.Contains(target)) { continue; } + if (target is not Item item) { continue; } + if (TargetItemComponent.IsEmpty) + { + item.GetComponents().ForEach(ic => Register(ic)); + } + else if (item.Components.FirstOrDefault(ic => ic.Name == TargetItemComponent) is ItemComponent targetItemComponent) + { + Register(targetItemComponent); + } + else + { +#if DEBUG + DebugConsole.ThrowError($"Failed to find the component {TargetItemComponent} on item {item.Prefab.Identifier}"); +#endif + } + } + void Register(ItemComponent ic) + { + targets.Add(ic.Item); + targetComponents.Add(ic); + ic.OnUsed.RegisterOverwriteExisting( + OnUseEventIdentifier, + i => { OnItemUsed(i.Item, i.User); }); + } + } + + private void DeregisterTargets() + { + foreach (ItemComponent ic in targetComponents) + { + ic.OnUsed.Deregister(OnUseEventIdentifier); + } + targetComponents.Clear(); + targets.Clear(); + } + + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + DeregisterTargets(); + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(WaitForItemUsedAction)} -> ({ItemTag})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs new file mode 100644 index 000000000..eea50bcf0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs @@ -0,0 +1,65 @@ +#nullable enable + +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Used to store logs of scripted events (a sort of "quest log") + /// + partial class EventLog + { + public class Event + { + public readonly Identifier EventIdentifier; + public readonly List Entries = new List(); + + public Event(Identifier eventPrefabId) + { + EventIdentifier = eventPrefabId; + } + } + + public class Entry + { + public readonly Identifier Identifier; + public string Text; + + public Entry(Identifier identifier, string text) + { + Identifier = identifier; + Text = text; + } + } + + private readonly Dictionary events = new Dictionary(); + + private bool TryAddEntryInternal(Identifier eventPrefabId, Identifier entryId, string text) + { + if (!events.TryGetValue(eventPrefabId, out Event? ev)) + { + ev = new Event(eventPrefabId); + events.Add(eventPrefabId, ev); + } + Entry? entry = ev.Entries.FirstOrDefault(e => e.Identifier == entryId); + if (entry == null) + { + ev.Entries.Add(new Entry(entryId, text)); + return true; + } + else if (entry.Text != text) + { + entry.Text = text; + return true; + } + return false; + } + + public void Clear() + { + events.Clear(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index b0655f113..c7c89bd26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -17,9 +17,24 @@ namespace Barotrauma CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, - UNLOCKPATH + UNLOCKPATH, + EVENTLOG, + EVENTOBJECTIVE, } + [NetworkSerialize] + public readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text) : INetSerializableStruct; + + [NetworkSerialize] + public readonly record struct NetEventObjective( + EventObjectiveAction.SegmentActionType Type, + Identifier Identifier, + Identifier ObjectiveTag, + Identifier TextTag, + Identifier ParentObjectiveId, + bool CanBeCompleted) : INetSerializableStruct; + + const float IntensityUpdateInterval = 5.0f; const float CalculateDistanceTraveledInterval = 5.0f; @@ -95,7 +110,7 @@ namespace Barotrauma get { return musicIntensity; } } - public List ActiveEvents + public IEnumerable ActiveEvents { get { return activeEvents; } } @@ -119,6 +134,8 @@ namespace Barotrauma private readonly List timeStamps = new List(); public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e)); + public readonly EventLog EventLog = new EventLog(); + public EventManager() { isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -198,7 +215,7 @@ namespace Barotrauma if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); - ActiveEvents.Add(newEvent); + activeEvents.Add(newEvent); } else { @@ -256,7 +273,18 @@ namespace Barotrauma CumulativeMonsterStrengthWrecks = 0; CumulativeMonsterStrengthCaves = 0; } - + + public void ActivateEvent(Event newEvent) + { + activeEvents.Add(newEvent); + newEvent.Init(); + } + + public void ClearEvents() + { + activeEvents.Clear(); + } + private void SelectSettings() { if (!EventManagerSettings.Prefabs.Any()) @@ -364,6 +392,14 @@ namespace Barotrauma } } + public void TriggerOnEndRoundActions() + { + foreach (var ev in activeEvents) + { + (ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f); + } + } + public void EndRound() { pendingEventSets.Clear(); @@ -478,14 +514,6 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab e) => - (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && - !level.LevelData.NonRepeatableEvents.Contains(e.Identifier) && - isFactionSuitable(e.Faction); - - bool isFactionSuitable(Identifier factionId) => - factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; - foreach (var subEventPrefab in eventSet.EventPrefabs) { foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers()) @@ -495,7 +523,7 @@ namespace Barotrauma } var suitablePrefabSubsets = eventSet.EventPrefabs.Where( - e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); + e => IsFactionSuitable(e.Faction, level) && e.EventPrefabs.Any(ep => IsSuitable(ep, level))).ToArray(); for (int i = 0; i < applyCount; i++) { @@ -512,7 +540,7 @@ namespace Barotrauma (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; if (eventPrefabs != null && random.NextDouble() <= probability) { - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.RandomSeed = randomSeed; @@ -529,10 +557,25 @@ namespace Barotrauma } if (eventSet.ChildSets.Any()) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); - if (newEventSet != null) + int setCount = eventSet.SubSetCount; + if (setCount > 1) { - CreateEvents(newEventSet); + var unusedSets = eventSet.ChildSets.ToList(); + for (int j = 0; j < setCount; j++) + { + var newEventSet = SelectRandomEvents(unusedSets, random: random); + if (newEventSet == null) { break; } + unusedSets.Remove(newEventSet); + CreateEvents(newEventSet); + } + } + else + { + var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); + if (newEventSet != null) + { + CreateEvents(newEventSet); + } } } } @@ -542,7 +585,7 @@ namespace Barotrauma { if (random.NextDouble() > probability) { continue; } - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } if (!selectedEvents.ContainsKey(eventSet)) @@ -636,6 +679,23 @@ namespace Barotrauma return null; } + public static bool IsSuitable(EventPrefab e, Level level) + { + return IsLevelSuitable(e, level) && IsFactionSuitable(e.Faction, level); + } + + public static bool IsLevelSuitable(EventPrefab e, Level level) + { + return + (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + !level.LevelData.NonRepeatableEvents.Contains(e.Identifier); + } + + private static bool IsFactionSuitable(Identifier factionId, Level level) + { + return factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; + } + private static bool IsValidForLevel(EventSet eventSet, Level level) { return @@ -1031,35 +1091,6 @@ namespace Barotrauma } } - /// - /// Finds all actions in a ScriptedEvent - /// - private static List> FindActions(ScriptedEvent scriptedEvent) - { - var list = new List>(); - foreach (EventAction eventAction in scriptedEvent.Actions) - { - list.AddRange(FindActionsRecursive(eventAction)); - } - - return list; - - static List> FindActionsRecursive(EventAction eventAction, int ident = 1) - { - var eventActions = new List> { Tuple.Create(ident, eventAction) }; - - ident++; - - foreach (var action in eventAction.GetSubActions()) - { - eventActions.AddRange(FindActionsRecursive(action, ident)); - } - - return eventActions; - } - } - - /// /// Get the entity that should be used in determining how far the player has progressed in the level. /// = The submarine or player character that has progressed the furthest. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 7bd5e7e01..e1c076d3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Reflection; -using System.Reflection.Emit; namespace Barotrauma { @@ -16,12 +15,25 @@ namespace Barotrauma public readonly float Commonness; public readonly Identifier BiomeIdentifier; public readonly Identifier Faction; - public readonly float SpawnDistance; + + public readonly LocalizedString Name; public readonly bool UnlockPathEvent; public readonly string UnlockPathTooltip; public readonly int UnlockPathReputation; + public static EventPrefab Create(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) + { + if (element.NameAsIdentifier() == nameof(TraitorEvent)) + { + return new TraitorEventPrefab(element, file, fallbackIdentifier); + } + else + { + return new EventPrefab(element, file, fallbackIdentifier); + } + } + public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) { @@ -40,6 +52,8 @@ namespace Barotrauma DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } + Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString()); + BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); @@ -49,19 +63,17 @@ namespace Barotrauma UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); - - SpawnDistance = element.GetAttributeFloat("spawndistance", 0); } public bool TryCreateInstance(out T instance) where T : Event { instance = CreateInstance() as T; - return instance is T; + return instance is not null; } public Event CreateInstance() { - ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) }); + ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType() }); Event instance = null; try { @@ -79,7 +91,7 @@ namespace Barotrauma public override string ToString() { - return $"EventPrefab ({Identifier})"; + return $"{nameof(EventPrefab)} ({Identifier})"; } public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index b5409cc22..e43b63056 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -94,6 +94,7 @@ namespace Barotrauma public readonly bool ChooseRandom; private readonly int eventCount = 1; + public readonly int SubSetCount = 1; private readonly Dictionary overrideEventCount = new Dictionary(); /// @@ -280,6 +281,7 @@ namespace Barotrauma ChooseRandom = element.GetAttributeBool("chooserandom", false); eventCount = element.GetAttributeInt("eventcount", 1); + SubSetCount = element.GetAttributeInt("setcount", 1); Exhaustible = element.GetAttributeBool("exhaustible", false); MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); @@ -295,7 +297,7 @@ namespace Barotrauma OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); - ResetTime = element.GetAttributeFloat("resettime", 0); + ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0); CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); @@ -474,7 +476,7 @@ namespace Barotrauma if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } - float spawnProbability = monsterEvent.Prefab.Probability; + float spawnProbability = monsterEvent.Prefab?.Probability ?? 0.0f; if (Rand.Value() > spawnProbability) { return; } int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 58d243d43..a208a7e50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -16,7 +16,7 @@ namespace Barotrauma protected readonly HashSet requireKill = new HashSet(); protected readonly HashSet requireRescue = new HashSet(); - private readonly string itemTag; + private readonly Identifier itemTag; private readonly XElement itemConfig; private readonly List items = new List(); @@ -90,7 +90,7 @@ namespace Barotrauma hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); itemConfig = prefab.ConfigElement.GetChildElement("Items"); - itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); + itemTag = prefab.ConfigElement.GetAttributeIdentifier("targetitem", Identifier.Empty); } protected override void StartMissionSpecific(Level level) @@ -118,7 +118,7 @@ namespace Barotrauma private void InitItems(Submarine submarine) { - if (!string.IsNullOrEmpty(itemTag)) + if (!itemTag.IsEmpty) { var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); if (!itemsToDestroy.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index d0cf6aea3..224dc40f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -73,7 +73,7 @@ namespace Barotrauma { get { - if (level.BeaconStation == null) + if (level.BeaconStation == null || state > 0) { yield break; } @@ -95,9 +95,10 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; } - if (item.GetComponent() != null || + bool isReactor = item.GetComponent() != null; + if ((isReactor && GameMain.GameSession is not { TraitorsEnabled: true }) || + item.GetComponent() != null || item.GetComponent() != null || - item.GetComponent() != null || item.GetComponent() != null) { item.InvulnerableToDamage = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 847a9a923..71dcf88a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -262,6 +262,12 @@ namespace Barotrauma SpawnedInCurrentOutpost = true, AllowStealing = false }; + item.AddTag("cargomission"); + item.AddTag(Prefab.Identifier); + foreach (var tag in Prefab.Tags) + { + item.AddTag(tag); + } item.FindHull(); items.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index daa064131..8eaba7820 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -23,6 +23,7 @@ namespace Barotrauma private readonly CharacterPrefab minionPrefab; private readonly Identifier spawnPointTag; + private WayPoint bossSpawnPoint; private readonly Identifier destructibleItemTag; private readonly string endCinematicSound; @@ -68,7 +69,13 @@ namespace Barotrauma { if (boss != null && !boss.Removed) { + Vector2 prevPos = boss.AnimController.Collider.SimPosition; boss.AnimController.ColliderIndex = 1; + if (bossSpawnPoint != null) + { + //ensure the new collider stays in the same position (the 2nd one has a different shape than the 1st one) + boss.AnimController.Collider.SetTransform(prevPos, 0.0f); + } } }, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2); } @@ -142,21 +149,21 @@ namespace Barotrauma protected override void StartMissionSpecific(Level level) { - var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); - if (spawnPoint == null) + bossSpawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); + if (bossSpawnPoint == null) { DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); return; } if (!IsClient) { - boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); + boss = Character.Create(bossPrefab.Identifier, bossSpawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); var minionList = new List(); float angle = 0; float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1); for (int i = 0; i < minionCount; i++) { - minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); + minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(bossSpawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); angle += angleStep; } SwarmBehavior.CreateSwarm(minionList.Cast()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 51dd38d21..10fa64cb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -315,27 +315,21 @@ namespace Barotrauma } } - private bool Survived(Character character) + private static bool Survived(Character character) { return IsAlive(character) && character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)); } - private bool IsAlive(Character character) + private static bool IsAlive(Character character) { return character != null && !character.Removed && !character.IsDead; } - private bool IsCaptured(Character character) - { - return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier); - } - protected override bool DetermineCompleted() { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c)); bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c)); bool vipDied = false; @@ -345,7 +339,7 @@ namespace Barotrauma vipDied = !Survived(vipCharacter); } - if (friendliesSurvived && !terroristsSurvived && !vipDied) + if (friendliesSurvived && !vipDied) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 90ac22989..fbbdc98c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -139,7 +139,7 @@ namespace Barotrauma { if (cave.Area.Contains(spawnedResource.WorldPosition)) { - cave.DisplayOnSonar = true; + cave.MissionsToDisplayOnSonar.Add(this); caves.Add(cave); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 5da1a323a..b2976762c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -95,7 +95,7 @@ namespace Barotrauma } } - public Dictionary ReputationRewards + public ImmutableList ReputationRewards { get { return Prefab.ReputationRewards; } } @@ -268,7 +268,7 @@ namespace Barotrauma delayedTriggerEvents.Clear(); foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) { - foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) + foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) { entityToShow.HiddenInGame = false; } @@ -353,8 +353,7 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { var newEvent = eventPrefab.CreateInstance(); - GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(); + GameMain.GameSession.EventManager.ActivateEvent(newEvent); } } @@ -372,7 +371,19 @@ namespace Barotrauma { ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); } - GiveReward(); + try + { + GiveReward(); + } + catch (Exception e) + { + string errorMsg = "Unknown error while giving mission rewards."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("Mission.End:GiveReward", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace); +#if SERVER + GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error); +#endif + } } TimesAttempted++; @@ -423,30 +434,7 @@ namespace Barotrauma crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); - int experienceGain = (int)(baseExperienceGain * experienceGainMultiplier.Value); -#if CLIENT - foreach (Character character in crewCharacters) - { - GiveMissionExperience(character.Info); - } -#else - foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) - { - //give the experience to the stored characterinfo if the client isn't currently controlling a character - GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo); - } - foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) - { - GiveMissionExperience(bot.Info); - } -#endif - - void GiveMissionExperience(CharacterInfo info) - { - var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); - info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); - info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); - } + DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value)); CalculateFinalReward(Submarine.MainSub); #if SERVER @@ -465,17 +453,32 @@ namespace Barotrauma character.Info.MissionsCompletedSinceDeath++; } - foreach (KeyValuePair reputationReward in ReputationRewards) + foreach (var reputationReward in ReputationRewards) { - if (reputationReward.Key == "location") + if (reputationReward.FactionIdentifier == "location") { - OriginLocation.Reputation?.AddReputation(reputationReward.Value); + OriginLocation.Reputation?.AddReputation(reputationReward.Amount); + TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction); } else { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); - float prevValue = faction.Reputation.Value; - faction?.Reputation.AddReputation(reputationReward.Value); + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier); + if (faction != null) + { + faction.Reputation.AddReputation(reputationReward.Amount); + TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction); + } + } + } + + void TryGiveReputationForOpposingFaction(Faction thisFaction, float amount) + { + if (MathUtils.NearlyEqual(amount, 0.0f)) { return; } + if (thisFaction?.Prefab != null && + !thisFaction.Prefab.OpposingFaction.IsEmpty) + { + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == thisFaction.Prefab.OpposingFaction); + faction?.Reputation.AddReputation(amount); } } } @@ -489,30 +492,10 @@ namespace Barotrauma } } -#if SERVER - public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) - { - int remainingRewards = totalReward; - float sum = GetRewardDistibutionSum(crew); - if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } - foreach (Character character in crew) - { - int rewardDistribution = character.Wallet.RewardDistribution; - float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; - int reward = (int)(totalReward * rewardWeight); - reward = Math.Min(remainingRewards, reward); - character.Wallet.Give(reward); - remainingRewards -= reward; - if (remainingRewards <= 0) { break; } - } - - return remainingRewards; - } -#endif + partial void DistributeExperienceToCrew(IEnumerable crew, int experienceGain); public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) { float sum = GetRewardDistibutionSum(crew, rewardDistribution); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 90e4d007b..5add25d3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -54,6 +53,20 @@ namespace Barotrauma { MissionType.Combat, typeof(CombatMission) } }; + public class ReputationReward + { + public readonly Identifier FactionIdentifier; + public readonly float Amount; + public readonly float AmountForOpposingFaction; + + public ReputationReward(XElement element) + { + FactionIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Amount = element.GetAttributeFloat(nameof(Amount), 0.0f); + AmountForOpposingFaction = element.GetAttributeFloat(nameof(AmountForOpposingFaction), 0.0f); + } + } + public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo, MissionType.End }; private readonly ConstructorInfo constructor; @@ -75,7 +88,7 @@ namespace Barotrauma public readonly Identifier AchievementIdentifier; - public readonly Dictionary ReputationRewards = new Dictionary(); + public readonly ImmutableList ReputationRewards; public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); @@ -252,7 +265,8 @@ namespace Barotrauma messages.Add(message); } } - + + List reputationRewards = new List(); int messageIndex = 0; foreach (var subElement in element.Elements()) { @@ -292,14 +306,7 @@ namespace Barotrauma break; case "reputation": case "reputationreward": - Identifier factionIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); - float amount = subElement.GetAttributeFloat("amount", 0.0f); - if (ReputationRewards.ContainsKey(factionIdentifier)) - { - DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Multiple reputation changes defined for the identifier \"{factionIdentifier}\"."); - continue; - } - ReputationRewards.Add(factionIdentifier, amount); + reputationRewards.Add(new ReputationReward(subElement)); break; case "metadata": Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); @@ -325,6 +332,7 @@ namespace Barotrauma } Headers = headers.ToImmutableArray(); Messages = messages.ToImmutableArray(); + ReputationRewards = reputationRewards.ToImmutableList(); Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); //backwards compatibility @@ -399,7 +407,7 @@ namespace Barotrauma else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins) { var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); - if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; } + if (connection?.LevelData == null || connection.LevelData.GenerationParams.GetMaxRuinCount() < 1) { return false; } } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index a9ab792ca..8b3d03131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -147,7 +147,7 @@ namespace Barotrauma monster.Params.AI.FleeHealthThreshold = 0; foreach (var targetParam in monster.Params.AI.Targets) { - if (targetParam.Tag.Equals("engine", StringComparison.OrdinalIgnoreCase)) { continue; } + if (targetParam.Tag == "engine") { continue; } switch (targetParam.State) { case AIState.Avoid: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index e849a5499..78f840cf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Voronoi2; namespace Barotrauma @@ -30,6 +29,8 @@ namespace Barotrauma private Vector2 nestPosition; + private Level.Cave selectedCave; + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { @@ -125,11 +126,9 @@ namespace Barotrauma } if (closestCave != null) { - closestCave.DisplayOnSonar = true; - SpawnNestObjects(level, closestCave); -#if SERVER selectedCave = closestCave; -#endif + selectedCave.MissionsToDisplayOnSonar.Add(this); + SpawnNestObjects(level, closestCave); } var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3); if (nearbyCells.Any()) @@ -172,8 +171,8 @@ namespace Barotrauma foreach (var subElement in itemConfig.Elements()) { - string itemIdentifier = subElement.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found"); continue; @@ -183,25 +182,34 @@ namespace Barotrauma float rotation = 0.0f; if (spawnEdges.Any()) { - var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); - spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); - Vector2 normal = Vector2.UnitY; - if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) + const float MinDistanceFromOtherItems = 30.0f; + const int MaxTries = 10; + for (int i = 0; i < MaxTries; i++) { - normal = edge.GetNormal(edge.Cell1); + var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); + spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); + Vector2 normal = Vector2.UnitY; + if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) + { + normal = edge.GetNormal(edge.Cell1); + } + else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid) + { + normal = edge.GetNormal(edge.Cell2); + } + spawnPos += normal * 10.0f; + rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2; + + if (items.All(it => Vector2.DistanceSquared(it.WorldPosition, spawnPos) > MinDistanceFromOtherItems)) { break; } } - else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid) - { - normal = edge.GetNormal(edge.Cell2); - } - spawnPos += normal * 10.0f; - rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2; } var item = new Item(itemPrefab, spawnPos, null); item.body.FarseerBody.BodyType = BodyType.Kinematic; item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation); item.FindHull(); + item.AddTag("nestmission"); + item.AddTag(Prefab.Identifier); items.Add(item); var statusEffectElement = @@ -286,7 +294,10 @@ namespace Barotrauma } //continue when all items are in the sub or destroyed - if (AllItemsDestroyedOrRetrieved()) { State = 1; } + if (AllItemsDestroyedOrRetrieved()) + { + State = 1; + } break; case 1: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index d52095f5b..80392f0ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -239,7 +239,7 @@ namespace Barotrauma private void InitPirateShip() { enemySub.NeutralizeBallast(); - if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent() is Reactor reactor) { reactor.PowerUpImmediately(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 7138946c3..7c54af4ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -30,8 +30,8 @@ namespace Barotrauma public readonly ItemPrefab ItemPrefab; public readonly Level.PositionType SpawnPositionType; - public readonly string ContainerTag; - public readonly string ExistingItemTag; + public readonly Identifier ContainerTag; + public readonly Identifier ExistingItemTag; public readonly bool RemoveItem; @@ -87,7 +87,7 @@ namespace Barotrauma public Target(ContentXElement element, SalvageMission mission) { this.mission = mission; - ContainerTag = element.GetAttributeString("containertag", ""); + ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty); RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); @@ -100,7 +100,7 @@ namespace Barotrauma .Fallback(TextManager.Get(sonarLabelTag)) .Fallback(element.GetAttributeString("sonarlabel", "")); } - ExistingItemTag = element.GetAttributeString("existingitemtag", ""); + ExistingItemTag = element.GetAttributeIdentifier("existingitemtag", Identifier.Empty); RemoveItem = element.GetAttributeBool("removeitem", true); @@ -109,7 +109,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); string itemName = element.GetAttributeString("itemname", ""); ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + if (ItemPrefab == null && ExistingItemTag.IsEmpty) { DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); } @@ -126,7 +126,7 @@ namespace Barotrauma string itemTag = element.GetAttributeString("itemtag", ""); ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; } - if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + if (ItemPrefab == null && ExistingItemTag.IsEmpty) { DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); } @@ -233,7 +233,7 @@ namespace Barotrauma Vector2.Zero : Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f); - if (!string.IsNullOrEmpty(target.ExistingItemTag)) + if (!target.ExistingItemTag.IsEmpty) { var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag)); if (GameMain.GameSession?.Missions != null) @@ -284,9 +284,9 @@ namespace Barotrauma if (target.Item == null) { - if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag)) + if (target.ItemPrefab == null && target.ContainerTag.IsEmpty) { - DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}"); + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}"); continue; } target.Item = new Item(target.ItemPrefab, position, null); @@ -312,8 +312,10 @@ namespace Barotrauma #endif } + target.Item.IsSalvageMissionItem = true; + //try to find a container and place the item inside it - if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null) + if (!target.ContainerTag.IsEmpty && target.Item.ParentInventory == null) { List validContainers = new List(); foreach (Item it in Item.ItemList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index b8a2f0936..364675efb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -218,7 +218,7 @@ namespace Barotrauma #endif } - private bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) + private static bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) { if (scanStatus.Value) { return false; } if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; } @@ -232,39 +232,15 @@ namespace Barotrauma switch (State) { case 0: - if (!AllTargetsScanned) { return; } - State = 1; - break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; + if (AllTargetsScanned) + { + State = 1; + } break; } } - protected override bool DetermineCompleted() - { - return State == 2 && AllScannersReturned(); - - bool AllScannersReturned() - { - foreach (var scanner in scanners) - { - if (scanner?.Item == null || scanner.Item.Removed) { return false; } - var owner = scanner.Item.GetRootInventoryOwner(); - if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player) - { - continue; - } - else if (owner is Character c && c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info)) - { - continue; - } - return false; - } - return true; - } - } + protected override bool DetermineCompleted() => State > 0; protected override void EndMissionSpecific(bool completed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index e45b7b4cc..00572c070 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using FarseerPhysics; namespace Barotrauma { @@ -13,6 +14,7 @@ namespace Barotrauma public readonly int MinAmount, MaxAmount; private readonly List monsters = new List(); + public readonly float SpawnDistance; private readonly float scatter; private readonly float offset; private readonly float delayBetweenSpawns; @@ -56,7 +58,7 @@ namespace Barotrauma } public MonsterEvent(EventPrefab prefab) - : base (prefab) + : base(prefab) { string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", ""); CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile); @@ -94,7 +96,7 @@ namespace Barotrauma } spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty); - + SpawnDistance = prefab.ConfigElement.GetAttributeFloat("spawndistance", 0); offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); @@ -174,6 +176,15 @@ namespace Barotrauma } } + public override string GetDebugInfo() + { + return + $"Finished: {IsFinished.ColorizeObject()}\n" + + $"Amount: {MinAmount.ColorizeObject()} - {MaxAmount.ColorizeObject()}\n" + + $"Spawn pending: {SpawnPending.ColorizeObject()}\n" + + $"Spawn position: {SpawnPos.ColorizeObject()}"; + } + private List GetAvailableSpawnPositions() { var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType)); @@ -214,13 +225,14 @@ namespace Barotrauma return availablePositions; } + private Level.InterestingPosition chosenPosition; private void FindSpawnPosition(bool affectSubImmediately) { if (disallowed) { return; } spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); - var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); + chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck); if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) { @@ -454,52 +466,109 @@ namespace Barotrauma { if (submarine.Info.Type != SubmarineType.Player) { continue; } float minDist = GetMinDistanceToSub(submarine); - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) + { + // Too close to a player sub. + return; + } } } - float minDistance = Prefab.SpawnDistance; - if (minDistance <= 0) + float spawnDistance = SpawnDistance; + if (spawnDistance <= 0) { if (SpawnPosType.HasFlag(Level.PositionType.Cave)) { - minDistance = 8000; + spawnDistance = 8000; } else if (SpawnPosType.HasFlag(Level.PositionType.Ruin)) { - minDistance = 5000; + spawnDistance = 5000; } else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation)) { - minDistance = 3000; + spawnDistance = 3000; } } - if (minDistance > 0) + if (spawnDistance > 0) { bool someoneNearby = false; foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance)) + float distanceSquared = Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value); + if (distanceSquared < MathUtils.Pow2(spawnDistance)) { someoneNearby = true; - break; + if (chosenPosition.Submarine != null) + { + Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine); + Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(submarine.WorldPosition, chosenPosition.Submarine, submarine); + if (CheckLineOfSight(from, to, chosenPosition.Submarine)) + { + // Line of sight to a player sub -> don't spawn yet. + return; + } + } + else + { + break; + } } } foreach (Character c in Character.CharacterList) { if (c == Character.Controlled || c.IsRemotePlayer) { - if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance)) + float distanceSquared = Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value); + if (distanceSquared < MathUtils.Pow2(spawnDistance)) { someoneNearby = true; - break; + if (chosenPosition.Submarine != null) + { + Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine); + Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(c.WorldPosition, chosenPosition.Submarine, c.Submarine); + if (CheckLineOfSight(from, to, chosenPosition.Submarine)) + { + // Line of sight to a player character -> don't spawn. Disable the event to prevent monsters "magically" spawning here. + Finish(); + return; + } + } + else + { + break; + } } } } if (!someoneNearby) { return; } + + static bool CheckLineOfSight(Vector2 from, Vector2 to, Submarine targetSub) + { + var bodies = Submarine.PickBodies(from, to, ignoredBodies: null, Physics.CollisionWall); + foreach (var b in bodies) + { + if (b.UserData is ISpatialEntity spatialEntity && spatialEntity.Submarine != targetSub) + { + // Different sub -> ignore + continue; + } + if (b.UserData is Structure s && !s.IsPlatform && s.CastShadow) + { + return false; + } + if (b.UserData is Item item && item.GetComponent() is Door door) + { + if (!door.IsBroken && !door.IsOpen) + { + return false; + } + } + } + return true; + } } - if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave)) { bool anyInAbyss = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 703aa19ce..85c735b0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Extensions; +using System; using System.Collections.Generic; using System.Linq; @@ -9,10 +10,19 @@ namespace Barotrauma private readonly Dictionary>> targetPredicates = new Dictionary>>(); private readonly Dictionary> cachedTargets = new Dictionary>(); + + /// + /// How many targets were there when they were tagged for the first time? Can be used by some EventActions to check how many entities + /// there are still left (e.g. how much of the initial cargo still exists) + /// + private readonly Dictionary initialAmounts = new Dictionary(); + private int prevEntityCount; private int prevPlayerCount, prevBotCount; private Character prevControlled; + public readonly OnRoundEndAction OnRoundEndAction; + private readonly string[] requiredDestinationTypes; public readonly bool RequireBeaconStation; @@ -20,16 +30,25 @@ namespace Barotrauma public List Actions { get; } = new List(); public Dictionary> Targets { get; } = new Dictionary>(); + protected virtual IEnumerable NonActionChildElementNames => Enumerable.Empty(); + public override string ToString() { - return $"ScriptedEvent ({prefab.Identifier})"; + return $"{nameof(ScriptedEvent)} ({prefab.Identifier})"; } public ScriptedEvent(EventPrefab prefab) : base(prefab) { foreach (var element in prefab.ConfigElement.Elements()) { - if (element.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) + Identifier elementId = element.Name.ToIdentifier(); + if (NonActionChildElementNames.Contains(elementId)) { continue; } + if (elementId == nameof(Barotrauma.OnRoundEndAction)) + { + OnRoundEndAction = EventAction.Instantiate(this, element) as OnRoundEndAction; + continue; + } + if (elementId == "statuseffect") { DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction."); continue; @@ -46,31 +65,125 @@ namespace Barotrauma requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); + var allActions = GetAllActions().Select(a => a.action); + foreach (var gotoAction in allActions.OfType()) + { + if (allActions.None(a => a is Label label && label.Name == gotoAction.Name)) + { + DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\"."); + } + } + GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start"); } + public override string GetDebugInfo() + { + EventAction currentAction = !IsFinished ? Actions[CurrentActionIndex] : null; + + string text = $"Finished: {IsFinished.ColorizeObject()}\n" + + $"Action index: {CurrentActionIndex.ColorizeObject()}\n" + + $"Current action: {currentAction?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n"; + + text += "All actions:\n"; + text += GetAllActions().Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.indent * 6)}{action.action.ToDebugString()}\n"); + + text += "Targets:\n"; + foreach (var (key, value) in Targets) + { + text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n"; + } + return text; + } + + public virtual string GetTextForReplacementElement(string tag) + { + if (tag.StartsWith("eventtag:")) + { + string targetTag = tag["eventtag:".Length..]; + Entity target = GetTargets(targetTag.ToIdentifier()).FirstOrDefault(); + if (target != null) + { + if (target is Item item) { return item.Name; } + if (target is Character character) { return character.Name; } + if (target is Hull hull) { return hull.DisplayName.Value; } + if (target is Submarine sub) { return sub.Info.DisplayName.Value; } + DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text."); + return target.ToString(); + } + else + { + return $"[target \"{targetTag}\" not found]"; + } + } + return string.Empty; + } + + public virtual LocalizedString ReplaceVariablesInEventText(LocalizedString str) + { + return str; + } + + /// + /// Finds all actions in the ScriptedEvent (recursively going through the subactions as well). + /// Returns a list of tuples where the first value is the indentation level (or "how deep in the hierarchy") the action is. + /// + public List<(int indent, EventAction action)> GetAllActions() + { + var list = new List<(int indent, EventAction action)>(); + foreach (EventAction eventAction in Actions) + { + list.AddRange(FindActionsRecursive(eventAction)); + } + return list; + + static List<(int indent, EventAction action)> FindActionsRecursive(EventAction eventAction, int indent = 1) + { + var eventActions = new List<(int indent, EventAction action)> { (indent, eventAction) }; + indent++; + foreach (var action in eventAction.GetSubActions()) + { + eventActions.AddRange(FindActionsRecursive(action, indent)); + } + return eventActions; + } + } + public void AddTarget(Identifier tag, Entity target) { if (target == null) { - throw new System.ArgumentException("Target was null"); + throw new ArgumentException($"Target was null (tag: {tag})"); } if (target.Removed) { - throw new System.ArgumentException("Target has been removed"); + throw new ArgumentException($"Target has been removed (tag: {tag})"); } - if (!Targets.ContainsKey(tag)) + if (Targets.ContainsKey(tag)) { - Targets.Add(tag, new List()); - } - Targets[tag].Add(target); - if (cachedTargets.ContainsKey(tag)) - { - cachedTargets[tag].Add(target); + if (!Targets[tag].Contains(target)) + { + Targets[tag].Add(target); + } } else { - cachedTargets.Add(tag, new List { target }); + Targets.Add(tag, new List() { target }); + } + if (cachedTargets.ContainsKey(tag)) + { + if (!cachedTargets[tag].Contains(target)) + { + cachedTargets[tag].Add(target); + } + } + else + { + cachedTargets.Add(tag, Targets[tag].ToList()); + } + if (!initialAmounts.ContainsKey(tag)) + { + initialAmounts.Add(tag, cachedTargets[tag].Count); } } @@ -88,6 +201,15 @@ namespace Barotrauma } } + public int GetInitialTargetCount(Identifier tag) + { + if (initialAmounts.TryGetValue(tag, out int count)) + { + return count; + } + return 0; + } + public IEnumerable GetTargets(Identifier tag) { if (cachedTargets.ContainsKey(tag)) @@ -136,10 +258,25 @@ namespace Barotrauma } } - cachedTargets.Add(tag, targetsToReturn); + cachedTargets.Add(tag, targetsToReturn); + if (!initialAmounts.ContainsKey(tag)) + { + initialAmounts.Add(tag, targetsToReturn.Count); + } return targetsToReturn; } + public void InheritTags(Entity originalEntity, Entity newEntity) + { + foreach (var kvp in Targets) + { + if (kvp.Value.Contains(originalEntity)) + { + kvp.Value.Add(newEntity); + } + } + } + public void RemoveTag(Identifier tag) { if (tag.IsEmpty) { return; } @@ -152,8 +289,14 @@ 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++; @@ -163,7 +306,7 @@ namespace Barotrauma botCount++; } } - if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) + if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) { cachedTargets.Clear(); prevEntityCount = Entity.EntityCount; @@ -204,6 +347,10 @@ namespace Barotrauma break; } } + if (CurrentActionIndex == -1) + { + DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event."); + } } if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs index 89fc4cefa..d8c7935d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs @@ -20,5 +20,35 @@ namespace Barotrauma.Extensions { return new Color(color.R, color.G, color.B, (byte)255); } + + private static bool IsFirstColorChannelDominant(byte first, byte second, byte third, float minimumRatio = 2) + => first > second * minimumRatio && first > third * minimumRatio; + + /// + /// Is the value of the red channel at least 'minimumRatio' larger than the blue and green + /// + public static bool IsRedDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0) + => color.A > minimumAlpha && + IsFirstColorChannelDominant( + first: color.R, + color.G, color.B, minimumRatio); + + /// + /// Is the value of the green channel at least 'minimumRatio' larger than the red and blue + /// + public static bool IsGreenDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0) + => color.A > minimumAlpha && + IsFirstColorChannelDominant( + first: color.G, + color.R, color.B, minimumRatio); + + /// + /// Is the value of the blue channel at least 'minimumRatio' larger than the red and green + /// + public static bool IsBlueDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0) + => color.A > minimumAlpha && + IsFirstColorChannelDominant( + first: color.B, + color.G, color.R, minimumRatio); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 272d38833..abbb65f70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -101,27 +101,9 @@ namespace Barotrauma.Extensions return source.Where(predicate).OrderBy(p => p.UintIdentifier).ToArray().GetRandom(randSync); } - - public static T RandomElementByWeight(this IList source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T GetRandomByWeight(this IEnumerable source, Func weightSelector, Rand.RandSync randSync) { - float totalWeight = source.Sum(weightSelector); - - float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight; - float currentWeightIndex = 0; - - for (int i = 0; i < source.Count; i++) - { - T weightedItem = source[i]; - float weight = weightSelector(weightedItem); - currentWeightIndex += weight; - - if (currentWeightIndex >= itemWeightIndex) - { - return weightedItem; - } - } - - return default; + return ToolBox.SelectWeightedRandom(source, weightSelector, randSync); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index 7322cb985..c6c3e3a92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -10,8 +10,8 @@ namespace Barotrauma public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s); public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s); - public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrEmpty() ?? true; - public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrEmpty() ?? true; + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrWhiteSpace() ?? true; public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index c44ded911..b87dd7027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -1,12 +1,20 @@ +#nullable enable using Barotrauma.Steam; using RestSharp; using System; using System.Net; +using System.Threading.Tasks; namespace Barotrauma { static partial class GameAnalyticsManager { + /// + /// The protocol used to communicate with the remote consent server may change. + /// This number tells the server which version the game is using so we can implement backwards-compatibility. + /// + private const string RemoteRequestVersion = "2"; + public enum Consent { /// @@ -42,13 +50,14 @@ namespace Barotrauma private static bool consentTextAvailable => TextManager.ContainsTag("statisticsconsentheader") && TextManager.ContainsTag("statisticsconsenttext"); + + private const string consentServerUrl = "https://barotraumagame.com/baromaster/"; + private const string consentServerFile = "consentserver.php"; - private readonly static string consentServerUrl = "https://barotraumagame.com/baromaster/"; - private readonly static string consentServerFile = "consentserver.php"; - - private static string GetAuthTicket() + private static async Task GetAuthTicket() { - Steamworks.AuthTicket authTicket = SteamManager.GetAuthSessionTicket(); + var ticketOption = await SteamManager.GetAuthTicketForGameAnalyticsConsent(); + if (!ticketOption.TryUnwrap(out var authTicket) || authTicket.Data is null) { return ""; } //convert byte array to hex return BitConverter.ToString(authTicket.Data).Replace("-", ""); } @@ -59,23 +68,27 @@ namespace Barotrauma /// the database or the user accepting via the privacy policy /// prompt should enable it. /// - public static void SetConsent(Consent consent) + public static void SetConsent(Consent consent, Action? onAnswerSent = null) { if (consent == Consent.Yes) { throw new Exception( "Cannot call SetConsent with value Consent.Yes, must only be set to this value via consent prompt"); } - SetConsentInternal(consent); + SetConsentInternal(consent, onAnswerSent); } /// /// Implementation of the bulk of SetConsent. /// DO NOT CALL THIS UNLESS NEEDED. /// - private static void SetConsentInternal(Consent consent) + private static void SetConsentInternal(Consent consent, Action? onAnswerSent) { - if (UserConsented == consent) { return; } + if (UserConsented == consent) + { + onAnswerSent?.Invoke(); + return; + } if (consent == Consent.Ask) { @@ -94,42 +107,72 @@ namespace Barotrauma ShutDown(); } + TaskPool.Add( + "GameAnalyticsConsent.SendAnswerToRemoteDatabase", + SendAnswerToRemoteDatabase(consent), + t => + { + onAnswerSent?.Invoke(); + if (!t.TryGetResult(out bool success) || !success) { return; } + + UserConsented = consent; + if (consent == Consent.Yes) + { + Init(); + } + }); + } + + /// + /// Try to send the user's response to the remote consent server. + /// Returns true upon success, false otherwise. + /// + private static async Task SendAnswerToRemoteDatabase(Consent consent) + { string authTicketStr; try { - authTicketStr = GetAuthTicket(); + authTicketStr = await GetAuthTicket(); } catch (Exception e) { DebugConsole.ThrowError("Error in GameAnalyticsManager.SetConsent. Could not get a Steam authentication ticket.", e); - return; + return false; } - RestClient client = null; + if (string.IsNullOrEmpty(authTicketStr)) + { + DebugConsole.ThrowError("Error in GameAnalyticsManager.SetContent. Steam authentication ticket was empty."); + return false; + } + + IRestResponse response; try { - client = new RestClient(consentServerUrl); + var client = new RestClient(consentServerUrl); + + var request = new RestRequest(consentServerFile, Method.GET); + request.AddParameter("authticket", authTicketStr); + request.AddParameter("action", "setconsent"); + request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + request.AddParameter("request_version", RemoteRequestVersion); + + response = await client.ExecuteAsync(request, Method.GET); } catch (Exception e) { DebugConsole.ThrowError("Error while connecting to consent server", e); + return false; } - if (client == null) { return; } - var request = new RestRequest(consentServerFile, Method.GET); - request.AddParameter("authticket", authTicketStr); - request.AddParameter("action", "setconsent"); - request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + if (!CheckResponse(response)) { return false; } - var response = client.Execute(request, Method.GET); - if (CheckResponse(response)) + if (!string.IsNullOrWhiteSpace(response.Content)) { - UserConsented = consent; - if (consent == Consent.Yes) - { - Init(); - } + DebugConsole.ThrowError($"Error in GameAnalyticsManager.SetContent. Consent server reported an error: {response.Content.Trim()}"); + return false; } + return true; } static partial void CreateConsentPrompt(); @@ -146,12 +189,6 @@ namespace Barotrauma return; } - static void error(string reason, Exception exception) - { - DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception); - SetConsent(Consent.Error); - } - if (!SteamManager.IsInitialized) { DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam)."); @@ -159,15 +196,33 @@ namespace Barotrauma return; } + TaskPool.Add( + "GameAnalyticsConsent.RequestAnswerFromRemoteDatabase", + RequestAnswerFromRemoteDatabase(), + t => + { + if (!t.TryGetResult(out Consent consent)) { return; } + SetConsentInternal(consent, onAnswerSent: null); + }); + } + + private static async Task RequestAnswerFromRemoteDatabase() + { + static void error(string reason, Exception exception) + { + DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception); + SetConsent(Consent.Error); + } + string authTicketStr; try { - authTicketStr = GetAuthTicket(); + authTicketStr = await GetAuthTicket(); } catch (Exception e) { error("Could not get a Steam authentication ticket.", e); - return; + return Consent.Error; } RestClient client; @@ -178,37 +233,36 @@ namespace Barotrauma catch (Exception e) { error("Error while connecting to consent server.", e); - return; + return Consent.Error; } var request = new RestRequest(consentServerFile, Method.GET); request.AddParameter("authticket", authTicketStr); request.AddParameter("action", "getconsent"); + request.AddParameter("request_version", RemoteRequestVersion); - TaskPool.Add($"{nameof(GameAnalyticsManager)}.{nameof(InitIfConsented)}", client.ExecuteAsync(request), (t) => + IRestResponse response; + try { - if (t.Exception != null) - { - error("Error executing the request to the consent server.", t.Exception.InnerException); - return; - } + response = await client.ExecuteAsync(request); + } + catch (Exception e) + { + error("Error executing the request to the consent server.", e.GetInnermost()); + return Consent.Error; + } - if (!t.TryGetResult(out IRestResponse response)) { return; } - if (!CheckResponse(response)) - { - SetConsent(Consent.Error); - } - else if (string.IsNullOrEmpty(response.Content)) - { - SetConsent(Consent.Ask); - } - else - { - SetConsentInternal(response.Content[0] == '1' - ? Consent.Yes - : Consent.No); - } - }); + if (!CheckResponse(response)) + { + return Consent.Error; + } + if (string.IsNullOrEmpty(response.Content)) + { + return Consent.Ask; + } + return response.Content[0] == '1' + ? Consent.Yes + : Consent.No; } private static bool CheckResponse(IRestResponse response) @@ -218,24 +272,24 @@ namespace Barotrauma DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString())); return false; } - else if (response.StatusCode != HttpStatusCode.OK) + + if (response.StatusCode == HttpStatusCode.OK) { return true; } + + switch (response.StatusCode) { - switch (response.StatusCode) - { - case HttpStatusCode.NotFound: - DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); - break; - case HttpStatusCode.ServiceUnavailable: - DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); - break; - default: - DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", - ("[statuscode]", response.StatusCode.ToString()), - ("[statusdescription]", response.StatusDescription))); - break; - } + case HttpStatusCode.NotFound: + DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); + break; + case HttpStatusCode.ServiceUnavailable: + DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); + break; + default: + DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", + ("[statuscode]", response.StatusCode.ToString()), + ("[statusdescription]", response.StatusDescription))); + break; } - return response.StatusCode == HttpStatusCode.OK; + return false; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 7312c3322..794567a60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -58,9 +58,13 @@ namespace Barotrauma } } - public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) + /// + /// Spawns loot in the specified container. + /// + /// Probability for an individual loot item to be skipped. I.e. a value of 1.0 means nothing spawns, 0.5 means there's about 50% of the normal amount of loot. + public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer, float skipItemProbability = 0.0f) { - CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); + CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer, skipItemProbability); } public static Identifier DefaultStartItemSet = new Identifier("normal"); @@ -105,6 +109,7 @@ namespace Barotrauma DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\""); continue; } + if (startItem.MultiPlayerOnly && GameMain.GameSession?.GameMode is { IsSinglePlayer: true }) { continue; } for (int i = 0; i < startItem.Amount; i++) { var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false); @@ -136,7 +141,7 @@ namespace Barotrauma } } - private static void CreateAndPlace(IEnumerable subs, ItemContainer regeneratedContainer = null) + private static void CreateAndPlace(IEnumerable subs, ItemContainer regeneratedContainer = null, float skipItemProbability = 0.0f) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -249,16 +254,16 @@ namespace Barotrauma } } - bool SpawnItems(ItemPrefab itemPrefab) + void SpawnItems(ItemPrefab itemPrefab, float skipItemProbability = 0.0f) { + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < skipItemProbability) { return; } if (itemPrefab == null) { string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return false; + return; } - bool success = false; bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) @@ -278,11 +283,9 @@ namespace Barotrauma if (newItems.Any()) { itemsToSpawn.AddRange(newItems); - success = true; } } } - return success; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index f14e1d6f4..f7273d22c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -440,7 +440,7 @@ namespace Barotrauma if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; } if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } + if (item.RootContainer is Item rootContainer && rootContainer.HasTag(Tags.DontSellItems)) { return false; } return true; }).Distinct(); @@ -498,7 +498,7 @@ namespace Barotrauma .Distinct(); public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) - => items.Where(it => it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + => items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) @@ -535,6 +535,12 @@ namespace Barotrauma DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!"); return null; } + if (!itemContainer.CanBeContained(item)) + { + // Can't contain the item in the crate -> let's not create it. + containerItem.Remove(); + return null; + } availableContainers.Add(itemContainer); #if SERVER if (GameMain.Server != null) @@ -607,7 +613,7 @@ namespace Barotrauma #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif - (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; + (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo); static void itemSpawned(PurchasedItem purchased, Item item) { Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index a397da922..c2f3a80d4 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,16 +306,15 @@ namespace Barotrauma } character.LoadTalents(); - - character.GiveIdCardTags(mainSubWaypoints[i]); - character.GiveIdCardTags(spawnWaypoints[i]); + character.GiveIdCardTags(new List() { mainSubWaypoints[i], 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/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index b08da08a6..548d30d55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -59,6 +59,8 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public Identifier OpposingFaction { get; } + public class HireableCharacter { public readonly Identifier NPCSetIdentifier; @@ -150,6 +152,7 @@ namespace Barotrauma Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); + OpposingFaction = element.GetAttributeIdentifier(nameof(OpposingFaction), Identifier.Empty); List hireableCharacters = new List(); List automaticMissions = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 9ff3c9aa4..fcf412f27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -95,6 +95,8 @@ namespace Barotrauma public SubmarineInfo PendingSubmarineSwitch; public bool TransferItemsOnSubSwitch { get; set; } + public bool SwitchedSubsThisRound { get; private set; } + protected Map map; public Map Map { @@ -293,6 +295,7 @@ namespace Barotrauma PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + SwitchedSubsThisRound = false; } public static int GetHullRepairCost() @@ -590,7 +593,7 @@ namespace Barotrauma /// protected abstract void LoadInitialLevel(); - protected abstract IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null); + protected abstract IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror); /// /// Which type of transition between levels is currently possible (if any) @@ -623,8 +626,7 @@ namespace Barotrauma } else if (map.SelectedConnection != null) { - nextLevel = Level.Loaded.LevelData != map.SelectedConnection?.LevelData || (map.SelectedConnection.Locations[0] == Level.Loaded.EndLocation == Level.Loaded.Mirrored) ? - map.SelectedConnection.LevelData : null; + nextLevel = map.SelectedConnection.LevelData; return TransitionType.ProgressToNextEmptyLocation; } else @@ -878,6 +880,19 @@ namespace Barotrauma port.Door.IsOpen = false; } } + + foreach (Item item in Item.ItemList) + { + if (item.Submarine is not Submarine sub) { continue; } + if (!sub.Info.IsPlayer) { continue; } + if (sub.TeamID != CharacterTeamType.Team1 && sub.TeamID != CharacterTeamType.Team2) { continue; } + if (item.GetComponent() is Reactor reactor && reactor.LastAIUser != null && reactor.LastUser == reactor.LastAIUser) + { + // Reactor managed by an AI crew -> + // Turn auto temp on, so that the reactor won't be unmanaged at beginning of the next round. + reactor.AutoTemp = true; + } + } } /// @@ -896,7 +911,7 @@ namespace Barotrauma { if (c.IsOnPlayerTeam) { - c.CharacterHealth.RemoveAllAfflictions(); + c.CharacterHealth.RemoveNegativeAfflictions(); } } foreach (LocationConnection connection in Map.Connections) @@ -904,13 +919,17 @@ namespace Barotrauma connection.Difficulty = connection.Biome.AdjustedMaxDifficulty; connection.LevelData = new LevelData(connection) { - IsBeaconActive = false + IsBeaconActive = false, + ForceOutpostGenerationParams = connection.LevelData.ForceOutpostGenerationParams }; connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds; } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty); + location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty) + { + ForceOutpostGenerationParams = location.LevelData.ForceOutpostGenerationParams + }; location.Reset(this); } Map.ClearLocationHistory(); @@ -1294,7 +1313,7 @@ namespace Barotrauma foreach (Submarine sub in subsToLeaveBehind) { GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); + MapEntity.MapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); LinkedSubmarine.CreateDummy(leavingSub, sub); } } @@ -1307,6 +1326,7 @@ namespace Barotrauma TransferItemsBetweenSubs(); } RefreshOwnedSubmarines(); + SwitchedSubsThisRound = true; PendingSubmarineSwitch = null; } @@ -1368,7 +1388,7 @@ namespace Barotrauma return; } // First move the cargo containers, so that we can reuse them - var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag("crate")); + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)); foreach (var (item, oldContainer) in cargoContainers) { Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 728dafe88..81a736eec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -230,6 +230,9 @@ namespace Barotrauma Bank = new Wallet(Option.None(), subElement); break; #if SERVER + case nameof(TraitorManager): + GameMain.Server?.TraitorManager?.Load(subElement); + break; case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 7c7c9e453..115d3dce2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -21,6 +21,8 @@ namespace Barotrauma public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; + public Version LastSaveVersion { get; set; } = GameMain.Version; + public readonly EventManager EventManager; public GameMode? GameMode; @@ -107,6 +109,10 @@ namespace Barotrauma public string? SavePath { get; set; } + public bool TraitorsEnabled => + GameMain.NetworkMember?.ServerSettings != null && + GameMain.NetworkMember.ServerSettings.TraitorProbability > 0.0f; + partial void InitProjSpecific(); private GameSession(SubmarineInfo submarineInfo) @@ -148,7 +154,8 @@ namespace Barotrauma this.SavePath = saveFile; GameMain.GameSession = this; XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); - //selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); + + LastSaveVersion = doc.Root.GetAttributeVersion("version", GameMain.Version); foreach (var subElement in rootElement.Elements()) { @@ -300,7 +307,7 @@ namespace Barotrauma public void LoadPreviousSave() { Submarine.Unload(); - SaveUtil.LoadGame(SavePath); + SaveUtil.LoadGame(SavePath ?? ""); } /// @@ -376,7 +383,10 @@ namespace Barotrauma missionPrefab.AllowedLocationTypes.Any() && !missionPrefab.AllowedConnectionTypes.Any()) { - LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); + Random rand = new MTRandom(ToolBox.StringToInt(levelSeed)); + LocationType? locationType = LocationType.Prefabs + .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)) + .GetRandom(rand); dummyLocations = CreateDummyLocations(levelSeed, locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; @@ -392,7 +402,7 @@ namespace Barotrauma DateTime startTime = DateTime.Now; #endif RoundDuration = 0.0f; - AfflictionPrefab.LoadAllEffects(); + AfflictionPrefab.LoadAllEffectsAndTreatmentSuitabilities(); MirrorLevel = mirrorLevel; if (SubmarineInfo == null) @@ -572,11 +582,11 @@ namespace Barotrauma } ReadyCheck.ReadyCheckCooldown = DateTime.MinValue; - GUI.PreventPauseMenuToggle = false; - HintManager.OnRoundStarted(); + EnableEventLogNotificationIcon(enabled: false); #endif + EventManager?.EventLog?.Clear(); if (campaignMode is { DivingSuitWarningShown: false } && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000) { @@ -605,7 +615,8 @@ namespace Barotrauma foreach (var sub in Submarine.Loaded) { - if (sub.Info.IsOutpost) + // TODO: Currently there's no need to check these on ruins, but that might change -> Could maybe just check if the body is static? + if (sub.Info.IsOutpost || sub.Info.IsBeacon || sub.Info.IsWreck) { sub.DisableObstructedWayPoints(); } @@ -759,7 +770,7 @@ namespace Barotrauma // Make sure that linked subs which are NOT docked to the main sub // (but still close enough to NOT be considered as 'left behind') // are also moved to keep their relative position to the main sub - var linkedSubs = MapEntity.mapEntityList.FindAll(me => me is LinkedSubmarine); + var linkedSubs = MapEntity.MapEntityList.FindAll(me => me is LinkedSubmarine); foreach (LinkedSubmarine ls in linkedSubs) { if (ls.Sub == null || ls.Submarine != Submarine) { continue; } @@ -845,7 +856,11 @@ namespace Barotrauma return characters.ToImmutableHashSet(); } - public void EndRound(string endMessage, List? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) +#if SERVER + private double LastEndRoundErrorMessageTime; +#endif + + public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { RoundEnding = true; @@ -854,10 +869,10 @@ namespace Barotrauma try { + EventManager?.TriggerOnEndRoundActions(); + ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); - int prevMoney = GetAmountOfMoney(crewCharacters); - foreach (Mission mission in missions) { mission.End(); @@ -898,11 +913,11 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) + if (GameMode is not TestGameMode && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) { GUI.ClearMessages(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); - GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType); + GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, transitionType, traitorResults); GUIMessageBox.MessageBoxes.Add(summaryFrame); RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; } @@ -915,6 +930,9 @@ namespace Barotrauma #endif SteamAchievementManager.OnRoundEnded(this); +#if SERVER + GameMain.Server?.TraitorManager?.EndRound(); +#endif GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); @@ -924,14 +942,16 @@ namespace Barotrauma #if CLIENT bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead); #else - bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); + bool success = + GameMain.Server != null && + GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, GameMode?.Preset.Identifier.Value ?? "none", RoundDuration); string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; - LogEndRoundStats(eventId); + LogEndRoundStats(eventId, traitorResults); if (GameMode is CampaignMode campaignMode) { GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); @@ -942,6 +962,19 @@ namespace Barotrauma #endif missions.Clear(); } + catch (Exception e) + { + string errorMsg = "Unknown error while ending the round."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("GameSession.EndRound:UnknownError", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace); +#if SERVER + if (Timing.TotalTime > LastEndRoundErrorMessageTime + 1.0) + { + GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error); + LastEndRoundErrorMessageTime = Timing.TotalTime; + } +#endif + } finally { RoundEnding = false; @@ -949,7 +982,7 @@ namespace Barotrauma int GetAmountOfMoney(IEnumerable crew) { - if (!(GameMode is CampaignMode campaign)) { return 0; } + if (GameMode is not CampaignMode campaign) { return 0; } return GameMain.NetworkMember switch { @@ -959,7 +992,7 @@ namespace Barotrauma } } - public void LogEndRoundStats(string eventId) + public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null) { GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); @@ -1003,6 +1036,12 @@ namespace Barotrauma } } + if (traitorResults.HasValue) + { + GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{traitorResults.Value.ObjectiveSuccessful}"); + GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier" : "TraitorUnidentified")}"); + } + foreach (Character c in GetSessionCrewCharacters(CharacterType.Both)) { foreach (var itemSelectedDuration in c.ItemSelectedDurations) @@ -1134,6 +1173,7 @@ namespace Barotrauma #warning TODO: after this gets on main, replace savetime with the commented line //rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow)); + LastSaveVersion = GameMain.Version; rootElement.Add(new XAttribute("version", GameMain.Version)); if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index b1effb055..c6522899c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -26,6 +26,8 @@ namespace Barotrauma public static readonly List AnySlot = new List() { InvSlotType.Any }; + public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand); + protected bool[] IsEquipped; /// @@ -92,7 +94,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for."); } #if DEBUG - else if (itemCount > capacity - 2) + else if (itemCount > capacity - 2 && !character.IsPet && capacity > 0) { DebugConsole.ThrowError( $"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " + @@ -132,6 +134,13 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } + else if (!character.Enabled) + { + foreach (var heldItem in character.HeldItems) + { + if (item.body != null) { item.body.Enabled = false; } + } + } }); } } @@ -207,6 +216,10 @@ namespace Barotrauma base.RemoveItem(item); #if CLIENT CreateSlots(); + if (character == Character.Controlled) + { + character.SelectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); + } #endif CharacterHUD.RecreateHudTextsIfControlling(character); //if the item was equipped and there are more items in the same stack, equip one of those items @@ -509,14 +522,20 @@ namespace Barotrauma if (character == Character.Controlled) { HintManager.OnObtainedItem(character, item); + character.SelectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); } #endif CharacterHUD.RecreateHudTextsIfControlling(character); if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) { - item.CampaignInteractionType = CampaignMode.InteractionType.None; + item.AssignCampaignInteractionType(CampaignMode.InteractionType.None); } } + protected override void CreateNetworkEvent(Range slotRange) + { + GameMain.NetworkMember?.CreateEntityEvent(character, new Character.InventoryStateEventData(slotRange)); + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 04a879f24..81ecc5a01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -975,9 +975,9 @@ namespace Barotrauma.Items.Components docked = false; + Item.Submarine.RefreshOutdoorNodes(); Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); obstructedWayPointsDisabled = false; - Item.Submarine.RefreshOutdoorNodes(); DockingTarget.Undock(); DockingTarget = null; @@ -1111,8 +1111,8 @@ namespace Barotrauma.Items.Components } if (!obstructedWayPointsDisabled && dockingState >= 0.99f) { - Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine); Item.Submarine.RefreshOutdoorNodes(); + Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine); obstructedWayPointsDisabled = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index e282bbb82..41ff58290 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -41,6 +41,8 @@ namespace Barotrauma.Items.Components } private bool isStuck; + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool IsStuck { get { return isStuck; } @@ -49,7 +51,10 @@ namespace Barotrauma.Items.Components if (isStuck == value) { return; } isStuck = value; #if SERVER - item.CreateServerEvent(this); + if (item.FullyInitialized) + { + item.CreateServerEvent(this); + } #endif } } @@ -105,7 +110,7 @@ namespace Barotrauma.Items.Components public bool CanBeWelded = true; private float stuck; - [Serialize(0.0f, IsPropertySaveable.No, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] public float Stuck { get { return stuck; } @@ -181,7 +186,11 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } - + + [ConditionallyEditable(ConditionallyEditable.ConditionType.HasIntegratedButtons), + Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")] + public bool ToggleWhenClicked { get; private set; } + public float OpenState { get { return openState; } @@ -342,8 +351,12 @@ namespace Barotrauma.Items.Components OnFailedToOpen(); return; } + item.SendSignal("1", "activate_out"); lastUser = user; - SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked); + if (ToggleWhenClicked) + { + SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked); + } } public override bool Select(Character character) @@ -389,8 +402,6 @@ namespace Barotrauma.Items.Components return; } - - bool isClosing = false; if ((!IsStuck && !IsJammed) || !isOpen) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index b039709e8..39fd2ed7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -18,8 +18,6 @@ namespace Barotrauma.Items.Components const int MaxNodes = 100; const float MaxNodeDistance = 150.0f; - private bool waitForVoltageRecalculation; - public struct Node { public Vector2 WorldPosition; @@ -140,10 +138,6 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } if (character != null && !CharacterUsable) { return false; } - CurrPowerConsumption = powerConsumption; - Voltage = 0.0f; - - waitForVoltageRecalculation = true; charging = true; timer = Duration; IsActive = true; @@ -159,12 +153,6 @@ namespace Barotrauma.Items.Components #if CLIENT frameOffset = Rand.Int(electricitySprite.FrameCount); #endif - if (waitForVoltageRecalculation) - { - waitForVoltageRecalculation = false; - return; - } - if (timer <= 0.0f) { if (reloadTimer > 0.0f) @@ -179,40 +167,49 @@ namespace Barotrauma.Items.Components timer -= deltaTime; if (charging) { - if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption) + bool hasPower = false; + if (item.Connections == null) { - List batteries = GetDirectlyConnectedBatteries(); - float neededPower = PowerConsumption; - while (neededPower > 0.0001f && batteries.Count > 0) + //no connections and can't be wired = must be powered by something like batteries + hasPower = Voltage > MinVoltage; + } + else + { + hasPower = GetAvailableInstantaneousBatteryPower() >= PowerConsumption; + } + + if (hasPower) + { + var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); + int batteryCount = batteries.Count(); + if (batteryCount > 0) { - batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); - float takePower = neededPower / batteries.Count; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) + float neededPower = PowerConsumption; + while (neededPower > 0.0001f) { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; - #if SERVER - if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } - #endif + float takePower = neededPower / batteryCount; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) + { + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; +#if SERVER + if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } +#endif + } } } Discharge(); - - } - else if (Voltage > MinVoltage) - { - Discharge(); } } } /// - /// Discharge coil only draws power when charging + /// Discharge coil doesn't consume grid power, directly takes from the batteries on its grid instead. /// - public override float GetCurrentPowerConsumption(Connection connection = null) + public override float GetCurrentPowerConsumption(Connection conn = null) { - return charging && IsActive ? PowerConsumption : 0; + return 0; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -294,10 +291,24 @@ namespace Barotrauma.Items.Components if (!submarinesInRange.Contains(structure.Submarine)) { continue; } if (OutdoorsOnly) { - //check if the structure is within a hull - //add a small offset away from the sub's center so structures right at the edge of a hull are still valid - Vector2 offset = Vector2.Normalize(structure.WorldPosition - structure.Submarine.WorldPosition); - if (Hull.FindHull(structure.Position + offset * Submarine.GridSize, useWorldCoordinates: false) != null) { continue; } + //check if there's a hull at either side of the wall + Vector2 normal = new Vector2( + (float)-Math.Sin(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation), + (float)Math.Cos(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation)); + Vector2 structurePos = structure.Position; + float offsetAmount = Submarine.GridSize.X * 2; + if (structure.HasBody) + { + structurePos = ConvertUnits.ToDisplayUnits(structure.Bodies.First().Position); + offsetAmount = Math.Max( + offsetAmount, + structure.IsHorizontal ? structure.BodyHeight : structure.BodyWidth); + } + if (Hull.FindHull(structurePos + normal * offsetAmount, useWorldCoordinates: false) != null && + Hull.FindHull(structurePos - normal * offsetAmount, useWorldCoordinates: false) != null) + { + continue; + } } } @@ -586,7 +597,7 @@ namespace Barotrauma.Items.Components case "trigger_in": if (signal.value != "0") { - item.Use(1.0f, null); + item.Use(1.0f); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 2ecabc5e7..f15afb13c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components var (x, y, z, w) = Parent.GrowthWeights; float[] weights = { x, y, z, w }; - value = pool.RandomElementByWeight(i => weights[i]); + value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced); } return (TileSide) (1 << value); @@ -416,8 +416,8 @@ namespace Barotrauma.Items.Components set => health = Math.Clamp(value, 0, MaxHealth); } - public bool Decayed; - public bool FullyGrown; + public bool Decayed { get; set; } + public bool FullyGrown { get; set; } private const int maxProductDelay = 10, maxVineGrowthDelay = 10; @@ -562,7 +562,7 @@ namespace Barotrauma.Items.Components if (spawnProduct && ProducedItems.Any()) { - SpawnItem(Item, ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); + SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 14d8699c1..bc06c7c4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -211,6 +211,9 @@ namespace Barotrauma.Items.Components #endif public bool DisableHeadRotation { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "If true, this item can't be used if the character is also holding a ranged weapon.")] + public bool DisableWhenRangedWeaponEquipped { get; set; } + [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] public float SpriteDepthWhenDropped { @@ -343,11 +346,9 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } - if (setTransform) - { - if (Pusher != null) { Pusher.Enabled = false; } - if (item.body != null) { item.body.Enabled = true; } - } + if (Pusher != null) { Pusher.Enabled = false; } + if (item.body != null) { item.body.Enabled = true; } + IsActive = false; attachTargetCell = null; @@ -616,6 +617,7 @@ namespace Barotrauma.Items.Components { //set to submarine-relative position item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false); + body.SetTransformIgnoreContacts(item.SimPosition, 0.0f); } item.Submarine = attachTarget.Submarine; } @@ -688,6 +690,7 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { + if (UsageDisabledByRangedWeapon(character)) { return false; } if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); } if (character != null) { @@ -792,19 +795,27 @@ namespace Barotrauma.Items.Components Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position; Vector2 attachPos = userPos + mouseDiff; + //offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor + //in the sub editor, we align the top-left corner of the item with the grid + //but here the origin of the item is placed at the attach position, so we need to offset it + Vector2 offset = new Vector2( + -(item.Rect.Width / 2) % Submarine.GridSize.X, + (item.Rect.Height / 2) % Submarine.GridSize.Y); + if (user.Submarine != null) { if (Submarine.PickBody( ConvertUnits.ToSimUnits(user.Position), ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null) { - attachPos = userPos + mouseDiff * Submarine.LastPickedFraction; + attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset; //round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall return new Vector2( - mouseDiff.X > 0 ? (float)Math.Floor(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X, - mouseDiff.Y > 0 ? (float)Math.Floor(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y); + (mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X, + (mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y) + - offset; } } else if (Level.Loaded != null) @@ -827,10 +838,9 @@ namespace Barotrauma.Items.Components } } - return - new Vector2( - MathUtils.RoundTowardsClosest(attachPos.X, Submarine.GridSize.X), - MathUtils.RoundTowardsClosest(attachPos.Y, Submarine.GridSize.Y)); + return new Vector2( + MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X), + MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset; } private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist) @@ -903,7 +913,7 @@ namespace Barotrauma.Items.Components { scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; - bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; + bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker); if (aim) { picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle); @@ -963,6 +973,15 @@ namespace Barotrauma.Items.Components } } + protected bool UsageDisabledByRangedWeapon(Character character) + { + if (DisableWhenRangedWeaponEquipped && character != null) + { + if (character.HeldItems.Any(it => it.GetComponent() != null)) { return true; } + } + return false; + } + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing @@ -1005,14 +1024,12 @@ namespace Barotrauma.Items.Components } else { - if (item.ParentInventory != null) + if (body != null) { - if (body != null) - { - item.body = body; - body.Enabled = false; - } - } + body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation); + item.body = body; + body.Enabled = item.ParentInventory == null; + } DeattachFromWall(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index eb57d4ba2..c482ec981 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -37,7 +37,20 @@ namespace Barotrauma.Items.Components [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public string OwnerName { get; set; } - + + private string ownerNameLocalized; + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string OwnerNameLocalized + { + get { return ownerNameLocalized; } + set + { + if (value.IsNullOrWhiteSpace()) { return; } + ownerNameLocalized = value; + OwnerName = TextManager.Get(value).Fallback(value).Value; + } + } + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Identifier OwnerJobId { get; set; } @@ -67,6 +80,9 @@ 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/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 353d25d9a..67c3ff90d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components private void CreateTriggerBody() { System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!"); - var body = item.body ?? holdable.Body; + var body = item.body ?? holdable?.Body; if (body != null && Attached) { trigger = new PhysicsBody(body.Width, body.Height, body.Radius, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 817f07d59..05fac61cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -189,7 +189,7 @@ namespace Barotrauma.Items.Components impactQueue.Clear(); return; } - if (picker == null && !picker.HeldItems.Contains(item)) + if (picker == null || !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; @@ -218,7 +218,8 @@ namespace Barotrauma.Items.Components AnimController ac = picker.AnimController; if (!hitting) { - bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; + bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && + !UsageDisabledByRangedWeapon(picker); if (aim) { UpdateSwingPos(deltaTime, out Vector2 swingPos); @@ -364,7 +365,7 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetStructure); } - else if (f2.Body.UserData is Item targetItem) + else if ((f2.Body.UserData as Item ?? f2.UserData as Item) is Item targetItem) { if (AllowHitMultiple) { @@ -410,7 +411,7 @@ namespace Barotrauma.Items.Components Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure; - Item targetItem = target.UserData as Item; + Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item; Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity; if (Attack != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 03f31ed8c..0e3083684 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components { //return if someone is already trying to pick the item if (pickTimer > 0.0f) { return false; } + if (PickingTime >= float.MaxValue) { return false; } if (picker == null || picker.Inventory == null) { return false; } if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; } @@ -112,10 +113,16 @@ namespace Barotrauma.Items.Components } } + public virtual bool OnPicked(Character picker) + { + return OnPicked(picker, pickDroppedStack: true); + } + + public virtual bool OnPicked(Character picker, bool pickDroppedStack) { //if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa) - if (item.GetComponents().Count() > 0) + if (item.GetComponents().Any()) { bool alreadyEquipped = false; for (int i = 0; i < picker.Inventory.Capacity; i++) @@ -132,11 +139,13 @@ namespace Barotrauma.Items.Components } if (alreadyEquipped) { return false; } } + var droppedStack = pickDroppedStack ? item.DroppedStack.ToList() : null; if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots)) { if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; } this.picker = picker; + for (int i = item.linkedTo.Count - 1; i >= 0; i--) { item.linkedTo[i].RemoveLinked(item); @@ -147,14 +156,22 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItem); + if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } PlaySound(ActionType.OnPicked, picker); #endif + if (pickDroppedStack) + { + foreach (var droppedItem in droppedStack) + { + if (droppedItem == item) { continue; } + droppedItem.GetComponent().OnPicked(picker, pickDroppedStack: false); + } + } return true; } #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } #endif return false; @@ -183,12 +200,15 @@ namespace Barotrauma.Items.Components } #if CLIENT - Character.Controlled?.UpdateHUDProgressBar( - this, - item.WorldPosition, - pickTimer / requiredTime, - GUIStyle.Red, GUIStyle.Green, - !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); + if (requiredTime < float.MaxValue) + { + Character.Controlled?.UpdateHUDProgressBar( + this, + item.WorldPosition, + pickTimer / requiredTime, + GUIStyle.Red, GUIStyle.Green, + !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); + } #endif picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f)); pickTimer += CoroutineManager.DeltaTime; @@ -197,12 +217,14 @@ namespace Barotrauma.Items.Components } StopPicking(picker); - - bool isNotRemote = true; + if (!item.Removed) + { + bool isNotRemote = true; #if CLIENT - isNotRemote = !picker.IsRemotePlayer; + isNotRemote = !picker.IsRemotePlayer; #endif - if (isNotRemote) OnPicked(picker); + if (isNotRemote) { OnPicked(picker); } + } yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 71d2510a9..e5dfda8a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -608,6 +608,7 @@ namespace Barotrauma.Items.Components float closestDist = float.MaxValue; foreach (Limb limb in targetCharacter.AnimController.Limbs) { + if (limb.Removed || limb.IgnoreCollisions || limb.Hidden || limb.IsSevered) { continue; } float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition); if (dist < closestDist) { @@ -716,7 +717,7 @@ namespace Barotrauma.Items.Components private readonly float repairTimeOut = 5; public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { - if (!(objective.OperateTarget is Gap leak)) + if (objective.OperateTarget is not Gap leak) { Reset(); return true; @@ -806,36 +807,50 @@ namespace Barotrauma.Items.Components // Press the trigger only when the tool is approximately facing the target. Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); + bool repair = true; if (angle < MathHelper.PiOver4) { if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i) - { - var door = i.GetComponent(); - // Hit a door, abandon so that we don't weld it shut. - return door != null && !door.CanBeTraversed; + { + if (i.GetComponent() is Door door && !door.CanBeTraversed ) + { + // Hit a door, don't repair so that we don't weld it shut. + if (door.Stuck > 90) + { + // Almost stuck -> just abandon. + return false; + } + if (door.Stuck > 50) + { + repair = false; + } + } } - // Check that we don't hit any friendlies - if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => + if (repair) { - if (hit.UserData is Character c) + // Check that we don't hit any friendlies + if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => { - if (c == character) { return false; } - return HumanAIController.IsFriendly(character, c); + if (hit.UserData is Character c) + { + if (c == character) { return false; } + return HumanAIController.IsFriendly(character, c); + } + return false; + })) + { + character.SetInput(InputType.Shoot, false, true); + Use(deltaTime, character); } - return false; - })) + } + repairTimer += deltaTime; + if (repairTimer > repairTimeOut) { - character.SetInput(InputType.Shoot, false, true); - Use(deltaTime, character); - repairTimer += deltaTime; - if (repairTimer > repairTimeOut) - { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); #endif - Reset(); - return true; - } + Reset(); + return true; } } } @@ -912,7 +927,7 @@ namespace Barotrauma.Items.Components // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in currentTargets) { - if (target is not Door door) { continue; } + if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } foreach (var propertyEffect in effect.PropertyEffects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index b70f89e16..c73ea2f41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components /// Vector2 DrawSize { get; } - void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1); + void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null); #endif } @@ -111,7 +111,6 @@ namespace Barotrauma.Items.Components private bool drawable = true; - #warning TODO: misnomer - should be IsActiveConditionalLogicalOperator [Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)] public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison { @@ -159,6 +158,12 @@ namespace Barotrauma.Items.Components protected set; } + [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)] + public bool LockGuiFramePosition { get; set; } + + [Serialize("0,0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)] + public Point GuiFrameOffset { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")] public bool CanBeSelected { @@ -251,6 +256,9 @@ namespace Barotrauma.Items.Components /// public float Speed => item.Speed; + public readonly record struct ItemUseInfo(Item Item, Character User); + public readonly NamedEvent OnUsed = new(); + public readonly bool InheritStatusEffects; public ItemComponent(Item item, ContentXElement element) @@ -339,6 +347,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "activeconditional": + case "isactiveconditional": case "isactive": IsActiveConditionals ??= new List(); IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement)); @@ -487,7 +496,7 @@ namespace Barotrauma.Items.Components case "trigger_in": if (signal.value != "0") { - item.Use(1.0f, signal.sender); + item.Use(1.0f, user: signal.sender); } break; case "toggle": @@ -524,7 +533,7 @@ namespace Barotrauma.Items.Components } item.ParentInventory.RemoveItem(item); } - Entity.Spawner.AddItemToRemoveQueue(item); + RemoveItem(item); } else { @@ -540,12 +549,23 @@ namespace Barotrauma.Items.Components } this.Item.ParentInventory.RemoveItem(this.Item); } - Entity.Spawner.AddItemToRemoveQueue(this.Item); + RemoveItem(this.Item); } else { this.Item.Condition += transferAmount; } + static void RemoveItem(Item item) + { + if (Screen.Selected is { IsEditor: true }) + { + item?.Remove(); + } + else + { + Entity.Spawner?.AddItemToRemoveQueue(item); + } + } } else { @@ -746,7 +766,7 @@ namespace Barotrauma.Items.Components /// private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard) { - if (item.Submarine != null) + if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { //id cards don't work in enemy subs (except on items that only require the default "idcard" tag) if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard")) @@ -906,7 +926,16 @@ namespace Barotrauma.Items.Components ParseMsg(); OverrideRequiredItems(componentElement); } - +#if CLIENT + if (GuiFrame != null) + { + GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset; + if (guiFrameDragHandle != null) + { + guiFrameDragHandle.Enabled = !LockGuiFramePosition; + } + } +#endif if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); } } @@ -922,6 +951,11 @@ namespace Barotrauma.Items.Components public virtual void OnScaleChanged() { } + /// + /// Called when the item has an ItemContainer and the contents inside of it changed. + /// + public virtual void OnInventoryChanged() { } + public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true) { Type type; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 74d18f5b9..e00d40f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -1,12 +1,12 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using Barotrauma.Abilities; using Barotrauma.Extensions; using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; using System.Collections.Immutable; -using Barotrauma.Abilities; +using System.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -14,11 +14,11 @@ namespace Barotrauma.Items.Components { readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); - readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); + readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); class SlotRestrictions { - public readonly int MaxStackSize; + public int MaxStackSize; public List ContainableItems; public readonly bool AutoInject; @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components private readonly List activeContainedItems = new List(); - private readonly List drawableContainedItems = new List(); + private readonly List containedItems = new List(); private List[] itemIds; @@ -128,6 +128,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")] + public bool QuickUseMovesItemsInside { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained { @@ -141,17 +144,28 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } + [Serialize(true, IsPropertySaveable.No)] + public bool AllowAccessWhenDropped { get; set; } + [Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } - private readonly HashSet containableRestrictions = new HashSet(); + private readonly HashSet containableRestrictions = new HashSet(); [Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] public string ContainableRestrictions { get { return string.Join(",", containableRestrictions); } set { - StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions); + containableRestrictions.Clear(); + if (!value.IsNullOrEmpty()) + { + foreach (var str in value.Split(',')) + { + if (str.IsNullOrWhiteSpace()) { continue; } + containableRestrictions.Add(str.ToIdentifier()); + } + } } } @@ -197,7 +211,6 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - /// /// Can be used by status effects to lock the inventory /// @@ -207,11 +220,28 @@ namespace Barotrauma.Items.Components set { Inventory.Locked = value; } } + /// + /// Can be used by status effects + /// + public int ContainedItemCount + { + get => Inventory.AllItems.Count(); + } + + /// + /// Can be used by status effects + /// + public int ContainedNonBrokenItemCount + { + get => Inventory.AllItems.Count(it => it.Condition > 0.0f); + } + private readonly ImmutableArray slotRestrictions; readonly List targets = new List(); - private Vector2 prevContainedItemPositions; + private float prevContainedItemRefreshRotation; + private Vector2 prevContainedItemRefreshPosition; private float autoInjectCooldown = 1.0f; const float AutoInjectInterval = 1.0f; @@ -330,6 +360,16 @@ namespace Barotrauma.Items.Components { slotRestrictions[i].ContainableItems = ContainableItems; } +#if CLIENT + if (element.GetChildElement("clearsubcontainerrestrictions") != null) + { + for (int i = capacity - MainContainerCapacity; i < capacity; i++) + { + slotRestrictions[i].MaxStackSize = MaxStackSize; + slotIcons[i] = null; + } + } +#endif } public int GetMaxStackSize(int slotIndex) @@ -363,12 +403,31 @@ namespace Barotrauma.Items.Components } var relatedItem = FindContainableItem(containedItem); - drawableContainedItems.RemoveAll(d => d.Item == containedItem); - drawableContainedItems.Add(new DrawableContainedItem(containedItem, - Hide: relatedItem?.Hide ?? false, - ItemPos: relatedItem?.ItemPos, - Rotation: relatedItem?.Rotation ?? 0.0f)); - drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item))); + var containedItemInfo = new ContainedItem(containedItem, + Hide: relatedItem?.Hide ?? false, + ItemPos: relatedItem?.ItemPos, + Rotation: relatedItem?.Rotation ?? 0.0f); + containedItems.RemoveAll(d => d.Item == containedItem); + + if (hideItems) + { + //if the items aren't visible, the draw order doesn't matter and we can skip the sorting + containedItems.Add(containedItemInfo); + } + else + { + int containedIndex = 0; + while (containedIndex < containedItems.Count) + { + if (index <= Inventory.FindIndex(containedItems[containedIndex].Item)) + { + break; + } + containedIndex++; + } + //sort drawables by their order in the inventory + containedItems.Insert(containedIndex, containedItemInfo); + } if (item.GetComponent() != null) { @@ -384,6 +443,14 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } + if (containedItem.FlippedX) + { + containedItem.FlipX(relativeToSub: false); + } + if (containedItem.FlippedY) + { + containedItem.FlipY(relativeToSub: false); + } item.SetContainedItemPositions(); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); @@ -397,7 +464,8 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); - drawableContainedItems.RemoveAll(i => i.Item == containedItem); + containedItems.RemoveAll(i => i.Item == containedItem); + item.SetContainedItemPositions(); //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); @@ -406,12 +474,14 @@ namespace Barotrauma.Items.Components public bool CanBeContained(Item item) { + if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; } return slotRestrictions.Any(s => s.MatchesItem(item)); } public bool CanBeContained(Item item, int index) { if (index < 0 || index >= capacity) { return false; } + if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; } return slotRestrictions[index].MatchesItem(item); } @@ -463,11 +533,7 @@ namespace Barotrauma.Items.Components if (item.ParentInventory is CharacterInventory ownerInventory) { - if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f) - { - SetContainedItemPositions(); - prevContainedItemPositions = item.Position; - } + SetContainedItemPositionsIfNeeded(); if (AutoInject || slotRestrictions.Any(s => s.AutoInject)) { @@ -508,11 +574,12 @@ namespace Barotrauma.Items.Components } } - else if (item.body != null && - item.body.Enabled && - item.body.FarseerBody.Awake) + else if (item.body != null && item.body.Enabled) { - SetContainedItemPositions(); + if (item.body.FarseerBody.Awake) + { + SetContainedItemPositionsIfNeeded(); + } } else if (activeContainedItems.Count == 0) { @@ -553,6 +620,20 @@ namespace Barotrauma.Items.Components } } + /// + /// Set the positions of the contained items if this item has moved/rotated enough + /// + private void SetContainedItemPositionsIfNeeded() + { + if (Vector2.DistanceSquared(prevContainedItemRefreshPosition, item.Position) > 10.0f || + Math.Abs(prevContainedItemRefreshRotation - item.body?.Rotation ?? item.RotationRad) > 0.01f) + { + SetContainedItemPositions(); + prevContainedItemRefreshPosition = item.Position; + prevContainedItemRefreshRotation = item.body?.Rotation ?? item.RotationRad; + } + } + public override void UpdateBroken(float deltaTime, Camera cam) { //update when the item is broken too to get OnContaining effects to execute and contained item positions to update @@ -662,10 +743,19 @@ namespace Barotrauma.Items.Components { SetContainedActive(true); } + else + { + SetContainedActive(false); + } } private void SetContainedActive(bool active) { + if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) && + (AllSubContainableItems == null || !AllSubContainableItems.Any(c => c.SetActive))) + { + return; + } foreach (Item containedItem in Inventory.AllItems) { RelatedItem containableItem = FindContainableItem(containedItem); @@ -722,7 +812,7 @@ namespace Barotrauma.Items.Components case "trigger_in": if (signal.value != "0") { - item.Use(1.0f, signal.sender); + item.Use(1.0f, user: signal.sender); } break; } @@ -786,7 +876,7 @@ namespace Barotrauma.Items.Components int i = 0; Vector2 currentItemPos = transformedItemPos; - foreach (DrawableContainedItem contained in drawableContainedItems) + foreach (ContainedItem contained in containedItems) { Vector2 itemPos = currentItemPos; if (contained.ItemPos.HasValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index a9f4467de..caa19f47c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -62,18 +62,58 @@ namespace Barotrauma.Items.Components public IEnumerable LimbPositions { get { return limbPositions; } } - [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)] public bool IsToggle { get; set; } - [Editable, Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] + private string output; + [ConditionallyEditable(ConditionallyEditable.ConditionType.HasConnectionPanel, onlyInEditors: false), + Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the controller is being activated or is toggled on. If empty, no signal is sent.", alwaysUseInstanceValues: true)] + public string Output + { + get { return output; } + set + { + if (value == null || value == output) { return; } + output = value; + //reactivate if signal isn't empty (we may not have been previously sending a signal, but might now) + if (!value.IsNullOrEmpty()) { IsActive = true; } + } + } + + private string falseOutput; + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: false), + Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the controller is toggled off. If empty, no signal is sent. Only valid if IsToggle is true.", alwaysUseInstanceValues: true)] + public string FalseOutput + { + get { return falseOutput; } + set + { + if (value == null || value == falseOutput) { return; } + falseOutput = value; + //reactivate if signal isn't empty (we may not have been previously sending a signal, but might now) + if (!value.IsNullOrEmpty()) { IsActive = true; } + } + } + + private bool state; + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: true), + Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] public bool State { - get; - set; + get { return state; } + set + { + if (state != value) + { + state = value; + string newOutput = state ? output : falseOutput; + IsActive = !string.IsNullOrEmpty(newOutput); + } + } } [Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")] @@ -164,10 +204,11 @@ namespace Barotrauma.Items.Components this.cam = cam; UserInCorrectPosition = false; - if (IsToggle) + string signal = IsToggle && State ? output : falseOutput; + if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal)) { - item.SendSignal(State ? "1" : "0", "signal_out"); - item.SendSignal(State ? "1" : "0", "trigger_out"); + item.SendSignal(signal, "signal_out"); + item.SendSignal(signal, "trigger_out"); } if (user == null @@ -183,7 +224,7 @@ namespace Barotrauma.Items.Components CancelUsing(user); user = null; } - if (!IsToggle || item.Connections == null) { IsActive = false; } + if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; } return; } @@ -313,9 +354,9 @@ namespace Barotrauma.Items.Components #endif } } - else + else if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal("1", sender: user), "trigger_out"); + item.SendSignal(new Signal(output, sender: user), "trigger_out"); } lastUsed = Timing.TotalTime; @@ -419,9 +460,9 @@ namespace Barotrauma.Items.Components #endif } } - else + else if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal("1", sender: picker), "signal_out"); + item.SendSignal(new Signal(output, sender: picker), "signal_out"); } #if CLIENT PlaySound(ActionType.OnUse, picker); @@ -480,6 +521,7 @@ namespace Barotrauma.Items.Components IsActive = false; CancelUsing(user); user = null; + return false; } else if (user.IsBot && !activator.IsBot) { @@ -500,8 +542,11 @@ namespace Barotrauma.Items.Components } #if SERVER item.CreateServerEvent(this); -#endif - item.SendSignal(new Signal("1", sender: user), "signal_out"); +#endif + if (!string.IsNullOrEmpty(output)) + { + item.SendSignal(new Signal(output, sender: user), "signal_out"); + } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index f8df97352..d5a5368ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, MaxOverVoltageFactor); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index af7668a08..0796cd686 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -24,6 +24,14 @@ namespace Barotrauma.Items.Components /// private float targetForce; + /// + /// Power demand of a marine engine is proportional with the cube of the square root of the thrusting force. + /// In practice meaning lower thrust is more effective at conserving power than it would be if the relationship between thrust and power consumption was linear. + /// Reverse exponent defined for use with overvoltage calculation: Supplying 2x power will result in 59% more force, 26% more speed, therefore 2x power. + /// + private const float ForceToPowerExponent = 3f / 2f; + private const float PowerToForceExponent = 1.0f / ForceToPowerExponent; + private float maxForce; private readonly Attack propellerDamage; @@ -130,7 +138,7 @@ namespace Barotrauma.Items.Components if (Math.Abs(Force) > 1.0f) { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); - float currForce = force * voltageFactor; + float currForce = force * MathF.Pow(voltageFactor, PowerToForceExponent); float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); @@ -180,7 +188,7 @@ namespace Barotrauma.Items.Components return 0; } - currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; + currPowerConsumption = MathF.Pow(Math.Abs(targetForce) / 100.0f, ForceToPowerExponent) * powerConsumption; //engines consume more power when in a bad condition item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); return currPowerConsumption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 71af0286b..ff6719a73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -92,6 +92,8 @@ namespace Barotrauma.Items.Components private readonly Dictionary fabricationLimits = new Dictionary(); + public Action OnItemFabricated; + public Fabricator(Item item, ContentXElement element) : base(item, element) { @@ -128,6 +130,11 @@ namespace Barotrauma.Items.Components } if (recipeInvalid) { continue; } + if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe)) + { + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\"."); + continue; + } fabricationRecipes.Add(recipe.RecipeHash, recipe); if (recipe.FabricationLimitMax >= 0) { @@ -327,7 +334,7 @@ namespace Barotrauma.Items.Components } } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; @@ -387,37 +394,35 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer) { - List foundAvailableItems = new List(); - foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) + List chosenIngredients = new List(); + var suitableIngredients = GetSortedSuitableIngredients(); + + foreach (var requiredItem in fabricatedItem.RequiredItems) { - for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) + for (int i = 0; i < requiredItem.Amount; i++) { - foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + foreach (var suitableIngredient in suitableIngredients) { - if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + if (!requiredItem.MatchesItem(suitableIngredient)) { continue; } + if (chosenIngredients.Contains(suitableIngredient)) { continue; } - var availableItems = availableIngredients[requiredPrefab.Identifier]; - var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage)); - - if (availableItem == null) { continue; } - - ingredientsStolen |= availableItem.StolenDuringRound; - if (!availableItem.AllowStealing) + ingredientsStolen |= suitableIngredient.StolenDuringRound; + if (!suitableIngredient.AllowStealing) { ingredientsAllowStealing = false; } //Leave it behind with reduced condition if it has enough to stay above 0 - if (requiredItem.UseCondition && availableItem.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) + if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) { - availableItem.Condition -= availableItem.Prefab.Health * requiredItem.MinCondition; + suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition; continue; } - if (availableItem.OwnInventory != null) + if (suitableIngredient.OwnInventory != null) { - foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod) + foreach (Item containedItem in suitableIngredient.OwnInventory.AllItemsMod) { - if (availableItem.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) + if (suitableIngredient.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) { Entity.Spawner.AddItemToRemoveQueue(containedItem); } @@ -427,15 +432,13 @@ namespace Barotrauma.Items.Components } } } - - foundAvailableItems.Add(availableItem); - availableItems.Remove(availableItem); + chosenIngredients.Add(suitableIngredient); break; } } } - var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems); + var fabricationIngredients = new AbilityFabricationItemIngredients(chosenIngredients); user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); foreach (Item availableItem in fabricationIngredients.Items) @@ -459,7 +462,7 @@ namespace Barotrauma.Items.Components { character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } - user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); quality = GetFabricatedItemQuality(fabricatedItem, user); } @@ -510,7 +513,7 @@ namespace Barotrauma.Items.Components } } - static void onItemSpawned(Item spawnedItem, Character user) + void onItemSpawned(Item spawnedItem, Character user) { if (user != null && user.TeamID != CharacterTeamType.None) { @@ -519,6 +522,7 @@ namespace Barotrauma.Items.Components wifiComponent.TeamID = user.TeamID; } } + OnItemFabricated?.Invoke(spawnedItem, user); } if (user?.Info != null && !user.Removed) { @@ -623,6 +627,11 @@ namespace Barotrauma.Items.Components } } + if (fabricableItem.HideForNonTraitors) + { + if (character is not { IsTraitor: true }) { return false; } + } + if (fabricableItem.RequiredMoney > 0) { switch (GameMain.GameSession?.GameMode) @@ -757,6 +766,19 @@ namespace Barotrauma.Items.Components itemList.AddRange(user.Inventory.AllItems); linkedInventories.Add(user.Inventory); } + foreach (Character c in Character.CharacterList) + { + //take materials from characters who've selected a linked container too + //(e.g. cabinet that's set to display alongside the fabricator UI) + if (c.SelectedItem != null && + c.Inventory != null && + linkedInventories.Contains(c.SelectedItem.OwnInventory) && + !linkedInventories.Contains(c.Inventory)) + { + itemList.AddRange(c.Inventory.AllItems); + linkedInventories.Add(c.Inventory); + } + } availableIngredients.Clear(); foreach (Item item in itemList) { @@ -765,34 +787,51 @@ namespace Barotrauma.Items.Components { availableIngredients[itemIdentifier] = new List(itemList.Count); } - //order by condition (prefer using worst-condition items) - int index = 0; - while (index < availableIngredients[itemIdentifier].Count && - compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0) - { - index++; - } - - static int compare(Item item1, Item item2, Inventory inputInventory) - { - bool item1InInputInventory = item1.ParentInventory == inputInventory; - bool item2InInputInventory = item2.ParentInventory == inputInventory; - //prefer items in the input inventory - if (item1InInputInventory != item2InInputInventory) - { - return item1InInputInventory ? 1 : -1; - } - else - { - float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0; - float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0; - //prefer items in worse condition - return Math.Sign(condition2 - condition1); - } - } - - availableIngredients[itemIdentifier].Insert(index, item); + availableIngredients[itemIdentifier].Add(item); } + foreach (var itemId in availableIngredients.Keys) + { + availableIngredients[itemId] = SortIngredients(availableIngredients[itemId]).ToList(); + } + } + + private IEnumerable SortIngredients(IEnumerable items) + { + return items + .OrderByDescending(getIngredientContainerPriority) + .ThenBy(it => it.Prefab.DefaultPrice?.Price ?? 0) + .ThenBy(it => MathUtils.IsValid(it.Condition) ? it.Condition : 0) + .ThenByDescending(it => it.ParentInventory?.FindIndex(it) ?? 0); + + int getIngredientContainerPriority(Item item) + { + if (item.ParentInventory == InputContainer.Inventory) + { + return 3; + } + else if (item.ParentInventory is CharacterInventory) + { + return 2; + } + return 1; + } + } + + private IEnumerable GetSortedSuitableIngredients() + { + List suitableIngredients = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) + { + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + { + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + var availableItems = availableIngredients[requiredPrefab.Identifier]; + suitableIngredients.AddRange( + availableItems.Where(potentialItem => requiredItem.IsConditionSuitable(potentialItem.ConditionPercentage))); + } + } + + return SortIngredients(suitableIngredients); } /// @@ -801,43 +840,34 @@ namespace Barotrauma.Items.Components /// private void MoveIngredientsToInputContainer(FabricationRecipe targetItem) { - //required ingredients that are already present in the input container - List usedItems = new List(); + List chosenIngredients = new List(); + var suitableIngredients = GetSortedSuitableIngredients(); - targetItem.RequiredItems.ForEach(requiredItem => { + foreach (var requiredItem in targetItem.RequiredItems) + { for (int i = 0; i < requiredItem.Amount; i++) { - foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + foreach (var suitableIngredient in suitableIngredients) { - if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + if (!requiredItem.MatchesItem(suitableIngredient)) { continue; } + if (chosenIngredients.Contains(suitableIngredient)) { continue; } - var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; - var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => + //in another inventory, we need to move the item + if (suitableIngredient.ParentInventory != inputContainer.Inventory) { - return !usedItems.Contains(potentialPrefab) && requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); - }); - if (availablePrefab == null) { continue; } - - availablePrefabs.Remove(availablePrefab); - - if (availablePrefab.ParentInventory == inputContainer.Inventory) - { - //already in input container, all good - usedItems.Add(availablePrefab); - } - else //in another inventory, we need to move the item - { - if (!inputContainer.Inventory.CanBePut(availablePrefab)) + if (!inputContainer.Inventory.CanBePut(suitableIngredient)) { - var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); + var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !chosenIngredients.Contains(it)); unneededItem?.Drop(null); } - inputContainer.Inventory.TryPutItem(availablePrefab, user: null); + inputContainer.Inventory.TryPutItem(suitableIngredient, user: null); } + chosenIngredients.Add(suitableIngredient); break; } } - }); + } + RefreshAvailableIngredients(); } @@ -884,6 +914,13 @@ namespace Barotrauma.Items.Components } savedFabricatedItem = null; } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + OnItemFabricated = null; + } + class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier { public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index c7d8b37e3..4b3e2e2cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -83,7 +83,7 @@ namespace Barotrauma.Items.Components hasPower = Voltage > MinVoltage; if (hasPower) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 0401bda5e..a6c4704cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); if (item.CurrentHull == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 1fdff1981..74aec6ffc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -203,6 +203,8 @@ namespace Barotrauma.Items.Components set; } + public bool MeltedDownThisRound { get; private set; } + public Reactor(Item item, ContentXElement element) : base(item, element) { @@ -282,7 +284,7 @@ namespace Barotrauma.Items.Components } prevAvailableFuel = AvailableFuel; - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); //use a smoothed "correct output" instead of the actual correct output based on the load //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily @@ -338,7 +340,7 @@ namespace Barotrauma.Items.Components { foreach (Item item in containedItems) { - if (!item.HasTag("reactorfuel")) { continue; } + if (!item.HasTag(Tags.Fuel)) { continue; } if (fissionRate > 0.0f) { bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost && @@ -648,6 +650,7 @@ namespace Barotrauma.Items.Components item.Condition = 0.0f; fireTimer = 0.0f; meltDownTimer = 0.0f; + MeltedDownThisRound = true; var containedItems = item.OwnInventory?.AllItems; if (containedItems != null) @@ -704,13 +707,13 @@ namespace Barotrauma.Items.Components var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true); containObjective.Completed += ReportFuelRodCount; containObjective.Abandoned += ReportFuelRodCount; - character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, "reactorfuel".ToIdentifier(), 30.0f); + character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f); void ReportFuelRodCount() { if (!character.IsOnPlayerTeam) { return; } if (character.Submarine != Submarine.MainSub) { return; } - int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); + int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1); if (remainingFuelRods == 0) { character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index daa50559e..f2c4b506b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components user = null; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float userSkill = 0.0f; if (user != null && controlledSub != null && @@ -508,7 +508,7 @@ namespace Barotrauma.Items.Components var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4); foreach (VoronoiCell cell in closeCells) { - if (cell.DoesDamage) + if (cell.DoesDamage || cell.Body is { BodyType: BodyType.Dynamic }) { foreach (GraphEdge edge in cell.Edges) { @@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components newAvoidStrength += avoid; debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation)); - if (dot > 0.0f) + if (dot > 0.0f && cell.DoesDamage) { showIceSpireWarning = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 45d2af075..8881ce000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -138,6 +138,8 @@ namespace Barotrauma.Items.Components set { flipIndicator = value; } } + public bool OutputDisabled { get; private set; } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; @@ -162,19 +164,19 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - adjustedCapacity = GetCapacity(); if (item.Connections == null) { IsActive = false; return; } + adjustedCapacity = GetCapacity(); isRunning = true; float chargeRatio = charge / adjustedCapacity; if (chargeRatio > 0.0f) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); } float loadReading = 0; @@ -242,6 +244,7 @@ namespace Barotrauma.Items.Components /// Minimum and maximum power output for the connection public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) { + if (OutputDisabled) { return PowerRange.Zero; } if (connection == powerOut) { float maxOutput; @@ -275,6 +278,7 @@ namespace Barotrauma.Items.Components /// public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) { + if (OutputDisabled) { return 0; } //Only power out connection can provide power and Max poweroutput can't be negative if (connection == powerOut && minMaxPower.Max > 0) { @@ -367,22 +371,27 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.IsPower) { return; } - - if (connection.Name == "set_rate") + switch (connection.Name) { - if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) - { - if (!MathUtils.IsValid(tempSpeed)) { return; } - - float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f); - RechargeSpeed = rechargeRate * MaxRechargeSpeed; -#if CLIENT - if (rechargeSpeedSlider != null) + case "disable_output": + OutputDisabled = signal.value != "0"; + break; + case "set_rate": + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { - rechargeSpeedSlider.BarScroll = rechargeRate; - } + if (!MathUtils.IsValid(tempSpeed)) { return; } + + float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f); + RechargeSpeed = rechargeRate * MaxRechargeSpeed; +#if CLIENT + if (rechargeSpeedSlider != null) + { + rechargeSpeedSlider.BarScroll = rechargeRate; + } #endif - } + } + break; + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index c1c82e68f..f023ff870 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components isBroken = false; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float powerReadingOut = 0; float loadReadingOut = ExtraLoad; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 88db0df19..8eda11f8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; #if CLIENT using Barotrauma.Sounds; #endif @@ -193,13 +194,13 @@ namespace Barotrauma.Items.Components { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); return; } if (Voltage > minVoltage) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); } #if CLIENT if (Voltage > minVoltage) @@ -666,7 +667,7 @@ namespace Barotrauma.Items.Components return conn1.IsPower && conn2.IsPower && conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f && - (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput); + (conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput); } /// @@ -682,6 +683,7 @@ namespace Barotrauma.Items.Components if (!recipient.IsPower || !recipient.IsOutput) { continue; } var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } + if (battery.OutputDisabled) { continue; } float maxOutputPerFrame = battery.MaxOutPut / 60.0f; float framesPerMinute = 3600.0f; availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame); @@ -689,23 +691,19 @@ namespace Barotrauma.Items.Components return availablePower; } - /// - /// Returns a list of batteries directly connected to the item - /// - protected List GetDirectlyConnectedBatteries() + protected IEnumerable GetDirectlyConnectedBatteries() { - List batteries = new List(); - if (item.Connections == null || powerIn == null) { return batteries; } - foreach (Connection recipient in powerIn.Recipients) + if (item.Connections != null && powerIn != null) { - if (!recipient.IsPower || !recipient.IsOutput) { continue; } - var battery = recipient.Item?.GetComponent(); - if (battery != null) + foreach (Connection recipient in powerIn.Recipients) { - batteries.Add(battery); + if (!recipient.IsPower || !recipient.IsOutput) { continue; } + if (recipient.Item?.GetComponent() is PowerContainer battery) + { + yield return battery; + } } } - return batteries; } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 1ad742343..b9146e660 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -56,6 +56,16 @@ namespace Barotrauma.Items.Components } } + enum StickTargetType + { + Structure, + Limb, + Item, + Submarine, + LevelWall, + Unknown + } + public const float WaterDragCoefficient = 0.1f; private readonly Queue impactQueue = new Queue(); @@ -353,7 +363,7 @@ namespace Barotrauma.Items.Components if (Item.Removed) { return; } launchPos = simPosition; //set the rotation of the projectile again because dropping the projectile resets the rotation - Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians)); + Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false); if (DeactivationTime > 0) { deactivationTimer = DeactivationTime; @@ -442,27 +452,49 @@ namespace Barotrauma.Items.Components { hits.Clear(); + Vector2 prevVelocity = item.body.LinearVelocity; + if (item.AiTarget != null) { item.AiTarget.SightRange = item.AiTarget.MaxSightRange; item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange; } + //do not create a network event about dropping the projectile at this point, + //otherwise clients would fail to launch the correct projectile when firing the weapon + var prevInventory = item.ParentInventory; + if (prevInventory != null && GameMain.NetworkMember is { IsServer: true }) + { + //update the state of the inventory after a delay, + //in case a client failed to launch the projectile for some reason + CoroutineManager.Invoke(() => + { + if (item.Removed) { return; } + prevInventory.CreateNetworkEvent(); + }, delay: CorrectionDelay / 2); + } item.Drop(null, createNetworkEvent: false); + Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; - item.body.Enabled = true; + item.body.Enabled = true; if (item.body.BodyType == BodyType.Kinematic) { item.body.LinearVelocity = impulse; } - else + else if (impulse.LengthSquared() > 0.001f) { impulse *= item.body.Mass; item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); } + else + { + //if no impulse is defined, maintain the projectile's original velocity (if any) + //can be used to make throwable items behave as projectiles + item.body.LinearVelocity = prevVelocity; + } item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; @@ -483,7 +515,7 @@ namespace Barotrauma.Items.Components float rotation = item.body.Rotation; Vector2 simPositon = item.SimPosition; Vector2 rayStartWorld = item.WorldPosition; - item.Drop(null); + item.Drop(null, createNetworkEvent: false); Item.WaterDragCoefficient = WaterDragCoefficient; item.body.Enabled = true; @@ -666,6 +698,7 @@ namespace Barotrauma.Items.Components if (fixture.Body.UserData is VoronoiCell) { return -1; } if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; } if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; } + if (fixture.Body == Level.Loaded?.TopBarrier || fixture.Body == Level.Loaded?.BottomBarrier) { return -1; } } // Ignore holdables that can't push -> shouldn't block @@ -891,8 +924,17 @@ namespace Barotrauma.Items.Components return false; } - Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? - contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); + Vector2 normalizedVel; + Vector2 dir; + if (item.body.LinearVelocity.LengthSquared() < 0.001f) + { + normalizedVel = Vector2.Zero; + dir = contact.Manifold.LocalNormal; + } + else + { + normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity); + } //do a raycast in the sub's coordinate space to see if it hit a structure var wallBody = Submarine.PickBody( @@ -901,7 +943,7 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionWall); if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) + Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) @@ -1136,7 +1178,13 @@ namespace Barotrauma.Items.Components removePending = true; item.HiddenInGame = true; item.body.FarseerBody.Enabled = false; - Entity.Spawner?.AddItemToRemoveQueue(item); + //delete with a brief delay so we don't delete the item + //before a client has managed to launch it (can happen with hitscan projectile) + CoroutineManager.Invoke(() => + { + if (item.Removed) { return; } + Entity.Spawner?.AddItemToRemoveQueue(item); + }, delay: 0.5f); } return true; @@ -1181,6 +1229,11 @@ namespace Barotrauma.Items.Components IgnoredBodies?.Clear(); } + public bool IsAttachedTo(PhysicsBody body) + { + return stickJoint != null && (stickJoint.BodyA == body?.FarseerBody || stickJoint.BodyB == body?.FarseerBody); + } + private void StickToTarget(Body targetBody, Vector2 axis) { if (stickJoint != null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs index d457cb589..5e330bd94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -1,12 +1,11 @@ using Microsoft.Xna.Framework; -using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class RemoteController : ItemComponent { [Serialize("", IsPropertySaveable.No, description: "Tag or identifier of the item that should be controlled.")] - public string Target + public Identifier Target { get; private set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 66e6e93e0..93721c36f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -14,7 +14,11 @@ namespace Barotrauma.Items.Components private readonly LocalizedString header; private float deteriorationTimer; - private float deteriorateAlwaysResetTimer; + public float ForceDeteriorationTimer + { + get; + private set; + } private int updateDeteriorationCounter; private const int UpdateDeteriorationInterval = 10; @@ -62,6 +66,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(60f, IsPropertySaveable.Yes, description: "How long will the item spontaneously deteriorate after being sabotaged.")] + public float SabotageDeteriorationDuration + { + get; + set; + } + [Serialize(80.0f, IsPropertySaveable.Yes, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float RepairThreshold { @@ -83,13 +94,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] - public bool DeteriorateAlways - { - get; - set; - } - private float skillRequirementMultiplier; [Serialize(1.0f, IsPropertySaveable.Yes)] @@ -394,20 +398,19 @@ namespace Barotrauma.Items.Components item.SendSignal(conditionSignal, "condition_out"); + if (ForceDeteriorationTimer > 0.0f) + { + ForceDeteriorationTimer -= deltaTime; + if (ForceDeteriorationTimer <= 0.0f) + { +#if SERVER + //let the clients know the deterioration delay + item.CreateServerEvent(this); +#endif + } + } if (CurrentFixer == null) { - if (deteriorateAlwaysResetTimer > 0.0f) - { - deteriorateAlwaysResetTimer -= deltaTime; - if (deteriorateAlwaysResetTimer <= 0.0f) - { - DeteriorateAlways = false; -#if SERVER - //let the clients know the deterioration delay - item.CreateServerEvent(this); -#endif - } - } updateDeteriorationCounter++; if (updateDeteriorationCounter >= UpdateDeteriorationInterval) { @@ -534,8 +537,7 @@ namespace Barotrauma.Items.Components } deteriorationTimer = 0.0f; - deteriorateAlwaysResetTimer = item.Condition / DeteriorationSpeed; - DeteriorateAlways = true; + ForceDeteriorationTimer = SabotageDeteriorationDuration; item.Condition = item.MaxCondition * (MinSabotageCondition / 100); wasGoodCondition = false; } @@ -553,7 +555,8 @@ namespace Barotrauma.Items.Components if (item.Condition <= 0.0f) { return; } if (!ShouldDeteriorate()) { return; } - if (deteriorationTimer > 0.0f) + //forced deterioration doesn't tick down the timer for spontaneous deterioration + if (deteriorationTimer > 0.0f && ForceDeteriorationTimer <= 0.0f) { if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -568,6 +571,7 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); } item.Condition -= deteriorationSpeed * deltaTime; } } @@ -592,7 +596,7 @@ namespace Barotrauma.Items.Components if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; } if (item.GetComponent() != null) { return true; } if (item.GetComponent() != null) { return true; } - if (item.HasTag("turretammosource")) { return true; } + if (item.HasTag(Tags.TurretAmmoSource)) { return true; } if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; } if (item.GetComponent() != null) { return true; } if (item.GetComponent() != null) { return true; } @@ -623,6 +627,8 @@ namespace Barotrauma.Items.Components private bool ShouldDeteriorate() { + if (ForceDeteriorationTimer > 0.0f) { return true; } + if (Level.IsLoadedFriendlyOutpost) { return false; } #if CLIENT if (GameMain.GameSession?.GameMode is TutorialMode) { return false; } @@ -666,13 +672,13 @@ namespace Barotrauma.Items.Components //oxygen generators don't deteriorate if they're not running if (oxyGenerator.CurrFlow > 0.1f) { return true; } } - else if (ic is Powered powered && !(powered is LightComponent)) + else if (ic is Powered powered && powered is not LightComponent) { if (powered.Voltage >= powered.MinVoltage) { return true; } } } - return DeteriorateAlways; + return false; } private float GetDeteriorationDelayMultiplier() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 64cc2add8..fd7fe8aca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components return; } - Vector2 diff = target.WorldPosition - source.WorldPosition; + Vector2 diff = target.WorldPosition - GetSourcePos(useDrawPosition: false); float lengthSqr = diff.LengthSquared(); if (lengthSqr > MaxLength * MaxLength) { @@ -372,7 +372,40 @@ namespace Barotrauma.Items.Components } } - private PhysicsBody GetBodyToPull(ISpatialEntity target) + /// + /// Get the position the rope starts from (taking into account barrel positions if needed) + /// + /// Should the interpolated draw position be used? If not, the WorldPosition is used. + private Vector2 GetSourcePos(bool useDrawPosition = false) + { + Vector2 sourcePos = source.WorldPosition; + if (source is Item sourceItem) + { + if (useDrawPosition) + { + sourcePos = sourceItem.DrawPosition; + } + if (!sourceItem.Removed) + { + if (sourceItem.GetComponent() is { } turret) + { + sourcePos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y); + } + else if (sourceItem.GetComponent() is { } weapon) + { + sourcePos += ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); + } + } + } + else if (useDrawPosition && source is Limb sourceLimb && sourceLimb.body != null) + { + sourcePos = sourceLimb.body.DrawPosition; + } + return sourcePos; + } + + + private static PhysicsBody GetBodyToPull(ISpatialEntity target) { if (target is Item targetItem) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs index 35b4a9d10..9f19704a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs new file mode 100644 index 000000000..f02fbebae --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -0,0 +1,684 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal sealed partial class CircuitBox : ItemComponent, IClientSerializable, IServerSerializable + { + public static readonly ImmutableHashSet UnrealiableOpcodes + = ImmutableHashSet.Create(CircuitBoxOpcode.Cursor); + + public ImmutableArray Inputs; + public ImmutableArray Outputs; + + public readonly List Components = new List(); + + public readonly List InputOutputNodes = new(); + + public readonly List Wires = new List(); + + public override bool IsActive => true; + + public Option FindInputOutputConnection(Identifier connectionName) + { + foreach (CircuitBoxInputConnection input in Inputs) + { + if (input.Name != connectionName) { continue; } + + return Option.Some(input); + } + + foreach (CircuitBoxOutputConnection output in Outputs) + { + if (output.Name != connectionName) { continue; } + + return Option.Some(output); + } + + return Option.None; + } + + public readonly ItemContainer[] containers; + + private const int ComponentContainerIndex = 0, + WireContainerIndex = 1; + + public ItemContainer? ComponentContainer + => GetContainerOrNull(ComponentContainerIndex); + + // wire container falls back to the main container if one isn't specified + public ItemContainer? WireContainer + => GetContainerOrNull(WireContainerIndex) ?? GetContainerOrNull(ComponentContainerIndex); + + public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true); + + public CircuitBox(Item item, ContentXElement element) : base(item, element) + { + containers = item.GetComponents().ToArray(); + if (containers.Length < 1) + { + DebugConsole.ThrowError("Circuit box must have at least one item container to function."); + } + + InitProjSpecific(element); + + var inputBuilder = ImmutableArray.CreateBuilder(); + var outputBuilder = ImmutableArray.CreateBuilder(); + + foreach (Connection conn in Item.Connections) + { + if (conn.IsOutput) + { + outputBuilder.Add(new CircuitBoxOutputConnection(Vector2.Zero, conn, this)); + } + else + { + inputBuilder.Add(new CircuitBoxInputConnection(Vector2.Zero, conn, this)); + } + } + + Inputs = inputBuilder.ToImmutable(); + Outputs = outputBuilder.ToImmutable(); + + InputOutputNodes.Add(new CircuitBoxInputOutputNode(Inputs, new Vector2(-512, 0f), CircuitBoxInputOutputNode.Type.Input, this)); + InputOutputNodes.Add(new CircuitBoxInputOutputNode(Outputs, new Vector2(512, 0f), CircuitBoxInputOutputNode.Type.Output, this)); + + item.OnDeselect += OnDeselected; + } + + /// + /// We want to load the components after the map has loaded since we need to link up the components to their items + /// and pretty much all items have higher ID than the circuit box. + /// + private Option delayedElementToLoad; + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + if (delayedElementToLoad.IsSome()) { return; } + delayedElementToLoad = Option.Some(componentElement); + } + + public override void OnInventoryChanged() + => OnViewUpdateProjSpecific(); + + public override void Update(float deltaTime, Camera cam) + { +#if CLIENT + // When loading from the server the wires cannot be properly loaded and connected up because we might not be loaded in properly yet. + // So we need to wait until the circuit box starts updating and then we can ensure the wires are connected. + if (wasInitializedByServer) + { + foreach (var w in Wires) + { + w.EnsureWireConnected(); + } + wasInitializedByServer = false; + } +#endif + TryInitializeNodes(); + } + + public override void OnMapLoaded() + => TryInitializeNodes(); + + private void TryInitializeNodes() + { + if (!delayedElementToLoad.TryUnwrap(out var loadElement)) { return; } + LoadFromXML(loadElement); + delayedElementToLoad = Option.None; + } + + private void LoadFromXML(ContentXElement loadElement) + { + foreach (var subElement in loadElement.Elements()) + { + string elementName = subElement.Name.ToString().ToLowerInvariant(); + switch (elementName) + { + case "component" when CircuitBoxComponent.TryLoadFromXML(subElement, this).TryUnwrap(out var comp): + Components.Add(comp); + break; + case "wire" when CircuitBoxWire.TryLoadFromXML(subElement, this).TryUnwrap(out var wire): + Wires.Add(wire); + break; + case "inputnode": + LoadFor(CircuitBoxInputOutputNode.Type.Input, subElement); + break; + case "outputnode": + LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement); + break; + } + } + +#if SERVER + // We need to let the clients know of the loaded data + if (needsServerInitialization) + { + CreateInitializationEvent(); + needsServerInitialization = false; + } +#endif + + void LoadFor(CircuitBoxInputOutputNode.Type type, ContentXElement subElement) + { + foreach (var node in InputOutputNodes) + { + if (node.NodeType != type) { continue; } + + node.Load(subElement); + break; + } + } + } + + public void CloneFrom(CircuitBox original, Dictionary clonedContainedItems) + { + Components.Clear(); + Wires.Clear(); + + foreach (var origComp in original.Components) + { + var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource); + Components.Add(newComponent); + } + + for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++) + { + var origNode = original.InputOutputNodes[ioIndex]; + var cloneNode = InputOutputNodes[ioIndex]; + + cloneNode.Position = origNode.Position; + } + + foreach (var origWire in original.Wires) + { + Option to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this), + from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); + + if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn)) + { + DebugConsole.ThrowError($"Error while cloning item \"{Name}\" - failed to find a connection for a wire. "); + continue; + } + + var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab); + Wires.Add(newWire); + } + } + + public override XElement Save(XElement parentElement) + { + XElement componentElement = base.Save(parentElement); + + foreach (CircuitBoxInputOutputNode node in InputOutputNodes) + { + componentElement.Add(node.Save()); + } + + foreach (CircuitBoxComponent node in Components) + { + componentElement.Add(node.Save()); + } + + foreach (CircuitBoxWire wire in Wires) + { + componentElement.Add(wire.Save()); + } + + return componentElement; + } + + public partial void OnDeselected(Character c); + + public record struct CreatedWire(CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, Option Item, ushort ID); + + public bool Connect(CircuitBoxConnection one, CircuitBoxConnection two, Action onCreated, ItemPrefab selectedWirePrefab) + { + if (!VerifyConnection(one, two)) { return false; } + + ushort id = ICircuitBoxIdentifiable.FindFreeID(Wires); + switch (one.IsOutput) + { + case true when !two.IsOutput: + { + CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(one), + end = CircuitBoxConnectorIdentifier.FromConnection(two); + + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + CreateWireWithoutItem(one, two, id, selectedWirePrefab); + onCreated(new CreatedWire(start, end, Option.None, id)); + return true; + } + + CreateWireWithItem(one, two, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id))); + return true; + } + case false when two.IsOutput: + { + CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(two), + end = CircuitBoxConnectorIdentifier.FromConnection(one); + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + CreateWireWithoutItem(two, one, id, selectedWirePrefab); + onCreated(new CreatedWire(start, end, Option.None, id)); + return true; + } + + CreateWireWithItem(two, one, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id))); + return true; + } + } + + return false; + } + + private static bool VerifyConnection(CircuitBoxConnection one, CircuitBoxConnection two) + { + if (one.IsOutput == two.IsOutput || one == two) { return false; } + + if (one is CircuitBoxNodeConnection oneNodeConnection && + two is CircuitBoxNodeConnection twoNodeConnection) + { + if (oneNodeConnection.Component == twoNodeConnection.Component) + { + return false; + } + } + + if (one is CircuitBoxNodeConnection { HasAvailableSlots: false } || + two is CircuitBoxNodeConnection { HasAvailableSlots: false }) + { + return one is not CircuitBoxNodeConnection || two is not CircuitBoxNodeConnection; + } + + return true; + } + + private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection); + + private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab) + { + bool hasExternalConnection = false; + if (one is CircuitBoxInputConnection input) + { + hasExternalConnection = true; + input.ExternallyConnectedTo.Add(two); + } + + if (two is CircuitBoxOutputConnection output) + { + hasExternalConnection = true; + one.Connection.CircuitBoxConnections.Add(output); + } + + if (hasExternalConnection) + { + two.ExternallyConnectedFrom.Add(one); + } + + AddWireDirect(id, prefab, Option.None, one, two); + } + + private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ItemPrefab prefab, ushort wireId, Action onItemSpawned) + { + if (WireContainer is null) { return; } + + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection."); + return; + } + + SpawnItem(this, prefab, WireContainer, wire => + { + AddWireDirect(wireId, prefab, Option.Some(wire), one, two); + onItemSpawned(wire); + }); + } + + private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort wireId, Item it) + { + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection."); + return; + } + + AddWireDirect(wireId, it.Prefab, Option.Some(it), one, two); + } + + private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) + => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); + + private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Action onItemSpawned) + { + if (id is ICircuitBoxIdentifiable.NullComponentID) + { + DebugConsole.ThrowError("Unable to add component because there are no free IDs."); + return false; + } + + if (ComponentContainer?.Inventory is { } inventory && inventory.HowManyCanBePut(prefab) <= 0) + { + DebugConsole.ThrowError("Unable to add component because there is no space in the inventory."); + return false; + } + + SpawnItem(this, prefab, ComponentContainer, spawnedItem => + { + Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); + onItemSpawned(spawnedItem); + }); + + OnViewUpdateProjSpecific(); + return true; + } + + // Unsafe because it doesn't perform error checking since it's data we get from the server + private void AddComponentInternalUnsafe(ushort id, Item backingItem, ItemPrefab usedResource, Vector2 pos) + { + Components.Add(new CircuitBoxComponent(id, backingItem, pos, this, usedResource)); + OnViewUpdateProjSpecific(); + } + + private static void ClearSelectionFor(ushort characterId, IReadOnlyCollection nodes) + { + foreach (var node in nodes) + { + if (node.SelectedBy != characterId) { continue; } + + node.SetSelected(Option.None); + } + } + + private void ClearAllSelectionsInternal(ushort characterId) + { + ClearSelectionFor(characterId, Components); + ClearSelectionFor(characterId, InputOutputNodes); + ClearSelectionFor(characterId, Wires); + } + + private void SelectComponentsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, Components); } + + if (!ids.Any()) { return; } + + foreach (CircuitBoxComponent node in Components) + { + if (!ids.Contains(node.ID)) { continue; } + + node.SetSelected(Option.Some(characterId)); + } + } + + private void UpdateSelections(ImmutableDictionary> nodeIds, + ImmutableDictionary> wireIds, + ImmutableDictionary> inputOutputs) + { + foreach (var wire in Wires) + { + if (!wireIds.TryGetValue(wire.ID, out var selectedBy)) { continue; } + + if (selectedBy.TryUnwrap(out var id)) + { + wire.IsSelected = true; + wire.SelectedBy = id; + continue; + } + + wire.IsSelected = false; + wire.SelectedBy = 0; + } + + foreach (var node in Components) + { + if (!nodeIds.TryGetValue(node.ID, out var selectedBy)) { continue; } + + node.SetSelected(selectedBy); + } + + foreach (var node in InputOutputNodes) + { + if (!inputOutputs.TryGetValue(node.NodeType, out var selectedBy)) { continue; } + + node.SetSelected(selectedBy); + } + } + + private void SelectWiresInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, Wires); } + + foreach (CircuitBoxWire wire in Wires) + { + if (!ids.Contains(wire.ID)) { continue; } + + wire.SetSelected(Option.Some(characterId)); + } + } + + private void SelectInputOutputInternal(IReadOnlyCollection io, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, InputOutputNodes); } + + foreach (var node in InputOutputNodes) + { + if (!io.Contains(node.NodeType)) { continue; } + + node.SetSelected(Option.Some(characterId)); + } + } + + private void RemoveComponentInternal(IReadOnlyCollection ids) + { + foreach (CircuitBoxComponent node in Components.ToImmutableArray()) + { + if (!ids.Contains(node.ID)) { continue; } + + Components.Remove(node); + node.Remove(); + + foreach (CircuitBoxWire wire in Wires.ToImmutableArray()) + { + if (node.Connectors.Contains(wire.From) || node.Connectors.Contains(wire.To)) + { + RemoveWireCollectionUnsafe(wire); + } + } + } + OnViewUpdateProjSpecific(); + } + + private void RemoveWireInternal(IReadOnlyCollection ids) + { + foreach (CircuitBoxWire wire in Wires.ToImmutableArray()) + { + if (!ids.Contains(wire.ID)) { continue; } + + RemoveWireCollectionUnsafe(wire); + } + + OnViewUpdateProjSpecific(); + } + + private void RemoveWireCollectionUnsafe(CircuitBoxWire wire) + { + foreach (CircuitBoxOutputConnection output in Outputs) + { + output.Connection.CircuitBoxConnections.Remove(wire.From); + } + + wire.From.Connection.CircuitBoxConnections.Remove(wire.To); + + if (wire.From is CircuitBoxInputConnection input) + { + input.ExternallyConnectedTo.Remove(wire.To); + } + + wire.To.ExternallyConnectedFrom.Remove(wire.From); + wire.From.ExternallyConnectedFrom.Remove(wire.To); + + wire.Remove(); + Wires.Remove(wire); + } + + private void MoveNodesInternal(IReadOnlyCollection ids, + IReadOnlyCollection ios, + Vector2 moveAmount) + { + IEnumerable nodes = Components.Where(node => ids.Contains(node.ID)); + foreach (CircuitBoxComponent node in nodes) + { + node.Position += moveAmount; + } + + + foreach (var io in InputOutputNodes) + { + if (!ios.Contains(io.NodeType)) { continue; } + io.Position += moveAmount; + } + + OnViewUpdateProjSpecific(); + } + + public override bool Select(Character character) + => item.GetComponent() is not { Attached: false } && base.Select(character); + + public partial void OnViewUpdateProjSpecific(); + + partial void InitProjSpecific(ContentXElement element); + + public override void ReceiveSignal(Signal signal, Connection connection) + { + foreach (var input in Inputs) + { + if (input.Connection != connection) { continue; } + + input.ReceiveSignal(signal); + break; + } + } + + public static bool IsRoundRunning() + => !Submarine.Unloading && GameMain.GameSession is { IsRunning: true }; + + public static Option FindCircuitBox(ushort itemId, byte componentIndex) + { + if (!IsRoundRunning() || Entity.FindEntityByID(itemId) is not Item item) { return Option.None; } + + if (componentIndex >= item.Components.Count) + { + return Option.None; + } + + ItemComponent targetComponent = item.Components[componentIndex]; + if (targetComponent is CircuitBox circuitBox) + { + return Option.Some(circuitBox); + } + + return Option.None; + } + + private ItemContainer? GetContainerOrNull(int index) => index >= 0 && index < containers.Length ? containers[index] : null; + + public void CreateRefundItemsForUsedResources(IReadOnlyCollection ids, Character? character) + { + if (!IsInGame()) { return; } + + var prefabsToCreate = Components.Where(comp => ids.Contains(comp.ID)) + .Select(static comp => comp.UsedResource) + .ToImmutableArray(); + + foreach (ItemPrefab prefab in prefabsToCreate) + { + if (character?.Inventory is null) + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, item.Position, item.Submarine); + } + else + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, character.Inventory); + } + } + } + + public static ImmutableArray GetSortedCircuitBoxSortedItemsFromPlayer(Character? character) + => character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true) + .OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit) + .ToImmutableArray() ?? ImmutableArray.Empty; + + public static bool CanItemBeAccessed(Item item) => + item.ParentInventory switch + { + ItemInventory ii => ii.Container.DrawInventory, + _ => true + }; + + public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character) + { + if (character is null) { return Option.None; } + + return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character)); + } + + public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray playerItems) + { + foreach (var invItem in playerItems) + { + if (invItem.Prefab == prefab || invItem.Prefab.Identifier == Tags.FPGACircuit) + { + return Option.Some(invItem); + } + } + + return Option.None; + } + + public static void SpawnItem(CircuitBox circuitBox, ItemPrefab prefab, ItemContainer? container, Action onSpawned) + { + if (container is null) + { + throw new Exception("Circuit box has no inventory"); + } + + if (IsInGame()) + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: onSpawned); + return; + } + + Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null); + container.Inventory.TryPutItem(forceSpawnedItem, null); + onSpawned(forceSpawnedItem); + } + + public static void RemoveItem(Item item) + { + if (IsInGame()) + { + Entity.Spawner?.AddItemToRemoveQueue(item); + return; + } + + item.Remove(); + } + + public static bool IsInGame() + => Screen.Selected is not { IsEditor: true }; + + public static bool IsCircuitBoxSelected(Character character) + => character.SelectedItem?.GetComponent() is not null; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 6f0e8a1cb..f1cc7d7ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -23,6 +23,15 @@ namespace Barotrauma.Items.Components private readonly HashSet wires; public IReadOnlyCollection Wires => wires; + /// + /// Circuit box input and output connections that are linked to this connection. + /// + /// + /// We don't want to create a wire between the circuit boxes connection panel and the + /// connection panel of the item inside the circuit box so we use this to bridge the gap. + /// + public List CircuitBoxConnections = new(); + private bool enumeratingWires; private readonly HashSet removedWires = new HashSet(); @@ -177,6 +186,12 @@ namespace Barotrauma.Items.Components } } + /// + /// Checks if the the connection is connected to a wire or a circuit box connection + /// + public bool IsConnectedToSomething() + => wires.Count > 0 || CircuitBoxConnections.Count > 0; + public void SetRecipientsDirty() { recipientsDirty = true; @@ -304,25 +319,15 @@ namespace Barotrauma.Items.Components if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; } signal.source?.LastSentSignalRecipients.Add(recipient); - - Connection connection = recipient; - connection.LastReceivedSignal = signal; #if CLIENT wire.RegisterSignal(signal, source: this); #endif + SendSignalIntoConnection(signal, recipient); + } - foreach (ItemComponent ic in recipient.item.Components) - { - ic.ReceiveSignal(signal, connection); - } - - if (recipient.Effects != null && signal.value != "0") - { - foreach (StatusEffect effect in recipient.Effects) - { - recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); - } - } + foreach (CircuitBoxConnection connection in CircuitBoxConnections) + { + connection.ReceiveSignal(signal); } enumeratingWires = false; foreach (var removedWire in removedWires) @@ -331,7 +336,24 @@ namespace Barotrauma.Items.Components } removedWires.Clear(); } - + + public static void SendSignalIntoConnection(Signal signal, Connection conn) + { + conn.LastReceivedSignal = signal; + + foreach (ItemComponent ic in conn.item.Components) + { + ic.ReceiveSignal(signal, conn); + } + + if (conn.Effects == null || signal.value == "0") { return; } + + foreach (StatusEffect effect in conn.Effects) + { + conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + } + } + public void ClearConnections() { if (IsPower && Grid != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 7c9fd3a1a..82c21a973 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -9,7 +9,8 @@ namespace Barotrauma.Items.Components { partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable { - public List Connections; + const int MaxConnectionCount = 256; + public readonly List Connections = new List(); private Character user; @@ -67,10 +68,13 @@ namespace Barotrauma.Items.Components public ConnectionPanel(Item item, ContentXElement element) : base(item, element) { - Connections = new List(); - foreach (var subElement in element.Elements()) { + if (Connections.Count == MaxConnectionCount) + { + DebugConsole.ThrowError($"Too many connections in the item {item.Prefab.Identifier} (> {MaxConnectionCount})."); + break; + } switch (subElement.Name.ToString()) { case "input": @@ -179,7 +183,7 @@ namespace Barotrauma.Items.Components { UpdateProjSpecific(deltaTime); - if (user == null || user.SelectedItem != item) + if (user == null || (user.SelectedItem != item && user.SelectedSecondaryItem != item)) { #if SERVER if (user != null) { item.CreateServerEvent(this); } @@ -208,6 +212,10 @@ namespace Barotrauma.Items.Components public bool CanRewire() { + if (item.Container?.GetComponent() != null) + { + return true; + } //attaching wires to items with a body is not allowed //(signal items remove their bodies when attached to a wall) if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic) @@ -395,7 +403,7 @@ namespace Barotrauma.Items.Components #if CLIENT TriggerRewiringSound(); #endif - + msg.WriteByte((byte)Connections.Count); foreach (Connection connection in Connections) { msg.WriteVariableUInt32((uint)connection.Wires.Count); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index eb76f17ea..2258bc176 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -299,9 +299,12 @@ namespace Barotrauma.Items.Components //make sure the clients know about the states of the checkboxes and text fields if (customInterfaceElementList.Any()) { - if (item.Submarine == null || !item.Submarine.Loading) + if (item.FullyInitialized) { - item.CreateServerEvent(this); + CoroutineManager.Invoke(() => + { + if (!item.Removed) { item.CreateServerEvent(this); } + }, delay: 0.1f); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 892a3afef..a4e549647 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index d2c88ab2c..f405963c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -198,6 +198,22 @@ namespace Barotrauma.Items.Components set; } + /// + /// Returns true if the red component of the light is twice as bright as the blue and green. Can be used by StatusEffects. + /// + public bool IsRed => ColorExtensions.IsRedDominant(LightColor); + + /// + /// Returns true if the green component of the light is twice as bright as the red and blue. Can be used by StatusEffects. + /// + public bool IsGreen => ColorExtensions.IsGreenDominant(LightColor); + + /// + /// Returns true if the blue component of the light is twice as bright as the red and green. Can be used by StatusEffects. + /// + public bool IsBlue => ColorExtensions.IsBlueDominant(LightColor); + + public float TemporaryFlickerTimer; public override void Move(Vector2 amount, bool ignoreContacts = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index eec77455a..b1d16e4d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; @@ -254,45 +254,52 @@ namespace Barotrauma.Items.Components } } - if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Pet) || Target.HasFlag(TargetType.Monster)) + bool triggerFromHumans = Target.HasFlag(TargetType.Human); + bool triggerFromPets = Target.HasFlag(TargetType.Pet); + bool triggerFromMonsters = Target.HasFlag(TargetType.Monster); + bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters; + if (!hasTriggers) { return; } + foreach (Character c in Character.CharacterList) { - foreach (Character c in Character.CharacterList) + if (IgnoreDead && c.IsDead) { continue; } + + //ignore characters that have spawned a second or less ago + //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground + if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } + if (c.IsHuman) { - if (IgnoreDead && c.IsDead) { continue; } - - //ignore characters that have spawned a second or less ago - //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground - if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - - if (c.IsHuman) - { - if (!Target.HasFlag(TargetType.Human)) { continue; } - } - else if (c.IsPet) - { - if (!Target.HasFlag(TargetType.Pet)) { continue; } - } - else - { - if (!Target.HasFlag(TargetType.Monster)) { continue; } - } - - //do a rough check based on the position of the character's collider first - //before the more accurate limb-based check - if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + if (!triggerFromHumans) { continue; } + } + else if (c.IsPet) + { + if (!triggerFromPets) { continue; } + } + else + { + // Not a human or a pet -> monster? + if (!triggerFromMonsters) { continue; } + if (CharacterParams.CompareGroup(c.Group, CharacterPrefab.HumanGroup)) { + //characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor) continue; } + } - foreach (Limb limb in c.AnimController.Limbs) + //do a rough check based on the position of the character's collider first + //before the more accurate limb-based check + if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + { + continue; + } + + foreach (Limb limb in c.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; } + if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { - if (limb.IsSevered) { continue; } - if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; } - if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) - { - MotionDetected = true; - return; - } + MotionDetected = true; + return; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index 33e8f4538..a78255abd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -50,6 +50,9 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] public bool UseCaptureGroup { get; set; } + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output the value of a capture group even if it's empty?", alwaysUseInstanceValues: true)] + public bool OutputEmptyCaptureGroup { get; set; } + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } @@ -120,6 +123,7 @@ namespace Barotrauma.Items.Components } string signalOut; + bool allowEmptyStringOutput = false; if (previousResult) { if (UseCaptureGroup) @@ -127,6 +131,7 @@ namespace Barotrauma.Items.Components if (previousGroups != null && previousGroups.TryGetValue(Output, out Group group)) { signalOut = group.Value; + allowEmptyStringOutput = OutputEmptyCaptureGroup; } else { @@ -143,13 +148,9 @@ namespace Barotrauma.Items.Components signalOut = FalseOutput; } - if (ContinuousOutput) + if (!string.IsNullOrEmpty(signalOut) || (allowEmptyStringOutput && signalOut == string.Empty)) { item.SendSignal(signalOut, "signal_out"); } + if (!ContinuousOutput) { - if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } - } - else - { - if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } nonContinuousOutputSent = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 5696c6280..c86f30ef2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get @@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components isBroken = false; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); if (Voltage > OverloadVoltage && CanBeOverloaded && item.Repairables.Any()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 4f09c6764..6b73ebd93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -10,11 +10,13 @@ namespace Barotrauma.Items.Components { public readonly string Text; public readonly Color Color; + public readonly bool IsWelcomeMessage; - public TerminalMessage(string text, Color color) + public TerminalMessage(string text, Color color, bool isWelcomeMessage) { Text = text; Color = color; + IsWelcomeMessage = isWelcomeMessage; } public void Deconstruct(out string text, out Color color) @@ -60,7 +62,7 @@ namespace Barotrauma.Items.Components set { if (string.IsNullOrEmpty(value)) { return; } - ShowOnDisplay(value, addToHistory: true, TextColor); + ShowOnDisplay(value, addToHistory: true, TextColor, isWelcomeMessage: false); } } @@ -113,7 +115,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - partial void ShowOnDisplay(string input, bool addToHistory, Color color); + partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage); public override void ReceiveSignal(Signal signal, Connection connection) { @@ -127,7 +129,7 @@ namespace Barotrauma.Items.Components signal.value = signal.value.Substring(0, MaxMessageLength); } string inputSignal = signal.value.Replace("\\n", "\n"); - ShowOnDisplay(inputSignal, addToHistory: true, TextColor); + ShowOnDisplay(inputSignal, addToHistory: true, TextColor, isWelcomeMessage: false); break; case "set_text_color": if (signal.value != prevColorSignal) @@ -160,7 +162,7 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed) { - ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); + ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor, isWelcomeMessage: true); DisplayedWelcomeMessage = ""; //disable welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) @@ -175,8 +177,13 @@ namespace Barotrauma.Items.Components var componentElement = base.Save(parentElement); for (int i = 0; i < messageHistory.Count; i++) { - componentElement.Add(new XAttribute("msg" + i, messageHistory[i].Text)); - componentElement.Add(new XAttribute("color" + i, messageHistory[i].Color.ToStringHex())); + var msg = messageHistory[i]; + componentElement.Add(new XAttribute("msg" + i, msg.Text)); + componentElement.Add(new XAttribute("color" + i, msg.Color.ToStringHex())); + if (msg.IsWelcomeMessage) + { + componentElement.Add(new XAttribute("welcomemessage" + i, true)); + } } return componentElement; } @@ -189,7 +196,8 @@ namespace Barotrauma.Items.Components string msg = componentElement.GetAttributeString("msg" + i, null); if (msg is null) { break; } Color color = componentElement.GetAttributeColor("color" + i, TextColor); - ShowOnDisplay(msg, addToHistory: true, color); + bool isWelcomeMessage = componentElement.GetAttributeBool("welcomemessage" + i, false); + ShowOnDisplay(msg, addToHistory: true, color, isWelcomeMessage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 10fec01b0..bb61ce491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -69,7 +69,10 @@ namespace Barotrauma.Items.Components public static int GetWaterPercentage(Hull hull) { - return hull.WaterVolume > 1.0f ? MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : 0; + //treat less than one pixel of water as "no water" + return hull.WaterVolume / hull.Rect.Width > 1.0f ? + MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : + 0; } public override void Update(float deltaTime, Camera cam) @@ -88,7 +91,7 @@ namespace Barotrauma.Items.Components //item in water -> we definitely want to send the True output isInWater = true; } - else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f && item.CurrentHull.WaterVolume > 1.0f) + else if (item.CurrentHull != null && GetWaterPercentage(item.CurrentHull) > 0) { //(center of the) item in not water -> check if the water surface is below the bottom of the item's rect if (item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index ebc87ccb7..a7117b73b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -89,6 +89,26 @@ namespace Barotrauma.Items.Components set; } + private float jamTimer; + public float JamTimer + { + get { return jamTimer; } + set + { + if (value > 0) + { +#if CLIENT + if (jamTimer <= 0) + { + HintManager.OnRadioJammed(Item); + } +#endif + IsActive = true; + } + jamTimer = Math.Max(0, value); + } + } + public WifiComponent(Item item, ContentXElement element) : base (item, element) { @@ -123,8 +143,12 @@ namespace Barotrauma.Items.Components } } - public bool CanTransmit() + public bool CanTransmit(bool ignoreJamming = false) { + if (!ignoreJamming) + { + if (jamTimer > 0) { return false; } + } return HasRequiredContainedItems(user: null, addMessage: false); } @@ -140,12 +164,13 @@ namespace Barotrauma.Items.Components { if (sender == null || sender.channel != channel) { return false; } if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } + if (jamTimer > 0) { return false; } //if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing // = no point in receiving if (!LinkToChat) { - if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0) + if (signalOutConnection == null || !signalOutConnection.IsConnectedToSomething()) { return false; } @@ -169,12 +194,16 @@ namespace Barotrauma.Items.Components if (sender == null || sender.channel != channel) { return false; } if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; } + if (jamTimer > 0) { return false; } return HasRequiredContainedItems(user: null, addMessage: false); } + public override void Update(float deltaTime, Camera cam) { chatMsgCooldown -= deltaTime; - if (chatMsgCooldown <= 0.0f) + JamTimer -= deltaTime; + ApplyStatusEffects(ActionType.OnActive, deltaTime); + if (chatMsgCooldown <= 0.0f && JamTimer <= 0.0f) { IsActive = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 39da66dbb..f96dc53e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -110,7 +110,14 @@ namespace Barotrauma.Items.Components get; set; } - + + [Serialize(true, IsPropertySaveable.Yes, "If disabled, the wire will not be dropped when connecting. Used in circuit box to store the wires inside the box.")] + public bool DropOnConnect + { + get; + set; + } + public Wire(Item item, ContentXElement element) : base(item, element) { @@ -224,26 +231,31 @@ namespace Barotrauma.Items.Components connections[connectionIndex] = newConnection; FixNodeEnds(); - if (addNode) + if (addNode) { AddNode(newConnection, connectionIndex); } SetConnectedDirty(); - if (connections[0] != null && connections[1] != null) + if (DropOnConnect) { - foreach (ItemComponent ic in item.Components) + if (connections[0] != null && connections[1] != null) { - if (ic == this) { continue; } - ic.Drop(null); + foreach (ItemComponent ic in item.Components) + { + if (ic == this) { continue; } + + ic.Drop(null); + } + + item.Container?.RemoveContained(item); + if (item.body != null) { item.body.Enabled = false; } + + IsActive = false; + + CleanNodes(); } - item.Container?.RemoveContained(item); - if (item.body != null) { item.body.Enabled = false; } - - IsActive = false; - - CleanNodes(); } if (item.body != null) { item.Submarine = newConnection.Item.Submarine; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 02f0807b8..9c29503ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -304,6 +304,21 @@ namespace Barotrauma.Items.Components set; } + private float _maxAngleOffset; + [Serialize(10.0f, IsPropertySaveable.No, description: "How much off the turret can be from the target for the AI to shoot. In degrees.")] + public float MaxAngleOffset + { + get => _maxAngleOffset; + private set => _maxAngleOffset = MathHelper.Clamp(value, 0f, 180f); + } + + [Serialize(1.1f, IsPropertySaveable.No, description: "How much does the AI prefer currently selected targets over new targets closer to the turret.")] + public float AICurrentTargetPriorityMultiplier + { + get; + private set; + } + [Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] public int MaxActiveProjectiles { @@ -383,7 +398,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = false; - isSlowTurret = item.HasTag("slowturret"); + isSlowTurret = item.HasTag("slowturret".ToIdentifier()); InitProjSpecific(element); } @@ -474,7 +489,7 @@ namespace Barotrauma.Items.Components } } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float previousChargeTime = currentChargeTime; @@ -594,9 +609,9 @@ namespace Barotrauma.Items.Components UpdateLightComponents(); - if (AutoOperate) + if (AutoOperate && ActiveUser == null) { - UpdateAutoOperate(deltaTime); + UpdateAutoOperate(deltaTime, ignorePower: false); } } @@ -687,7 +702,7 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); if (projectileContainer != null && projectileContainer.Item != item) { - projectileContainer?.Item.Use(deltaTime, null); + projectileContainer?.Item.Use(deltaTime); } } else @@ -713,7 +728,7 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - containerItem.Use(deltaTime, null); + containerItem.Use(deltaTime); projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { return true; } } @@ -749,7 +764,7 @@ namespace Barotrauma.Items.Components { if (e is not Item linkedItem) { continue; } if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } - if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) + if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag(Tags.TurretAmmoSource)) { tinkeringStrength = repairable.TinkeringStrength; } @@ -757,28 +772,29 @@ namespace Barotrauma.Items.Components if (!ignorePower) { - List batteries = GetDirectlyConnectedBatteries(); - float neededPower = GetPowerRequiredToShoot(); - - // tinkering is currently not factored into the common method as it is checked only when shooting - // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later - neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); - - while (neededPower > 0.0001f && batteries.Count > 0) + var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); + int batteryCount = batteries.Count(); + if (batteryCount > 0) { - batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); - if (!batteries.Any()) { break; } - float takePower = neededPower / batteries.Count; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) + float neededPower = GetPowerRequiredToShoot(); + // tinkering is currently not factored into the common method as it is checked only when shooting + // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later + neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); + while (neededPower > 0.0001f) { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; + float takePower = neededPower / batteryCount; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) + { + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; #if SERVER - battery.Item.CreateServerEvent(battery); + battery.Item.CreateServerEvent(battery); #endif + } } } + } launchedProjectile = projectiles.FirstOrDefault(); @@ -891,9 +907,21 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); - projectile.SetTransform( - ConvertUnits.ToSimUnits(GetRelativeFiringPosition()), - -(launchRotation ?? rotation) + spread); + + Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition()); + + //check if there's some other sub between the turret's origin and the launch pos, + //and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub + Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true, + customPredicate: (Fixture f) => + { + return f.Body.UserData is not Submarine sub || sub != item.Submarine; + }); + if (pickedBody != null) + { + launchPos = Submarine.LastPickedPosition; + } + projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -959,8 +987,15 @@ namespace Barotrauma.Items.Components private float updateTimer; private bool updatePending; - public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default) + private float GetTargetPriorityModifier() => currentChargingState == ChargingState.WindingUp ? 10f : AICurrentTargetPriorityMultiplier; + + public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag = default) { + if (!ignorePower && !HasPowerToShoot()) + { + return; + } + IsActive = true; if (friendlyTag.IsEmpty) @@ -1006,8 +1041,12 @@ namespace Barotrauma.Items.Components if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; } float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } - if (!CheckTurretAngle(character.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(character.WorldPosition)) { continue; } target = character; + if (currentTarget != null && target == currentTarget) + { + priority *= GetTargetPriorityModifier(); + } closestDist = dist / priority; } } @@ -1022,8 +1061,12 @@ namespace Barotrauma.Items.Components if (dist > closestDist) { continue; } if (dist > shootDistance * shootDistance) { continue; } if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } - if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; } target = targetItem; + if (currentTarget != null && target == currentTarget) + { + priority *= GetTargetPriorityModifier(); + } closestDist = dist / priority; } } @@ -1090,10 +1133,10 @@ namespace Barotrauma.Items.Components } } if (target == null) { return; } - + currentTarget = target; + float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); targetRotation = MathUtils.WrapAngleTwoPi(angle); - if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; } if (target is Hull targetHull) @@ -1106,10 +1149,8 @@ namespace Barotrauma.Items.Components } else { - if (!CheckTurretAngle(angle)) { return; } - float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); - float turretAngle = -rotation; - if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } + if (!IsWithinAimingRadius(angle)) { return; } + if (!IsPointingTowards(target.WorldPosition)) { return; } } Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); @@ -1129,7 +1170,7 @@ namespace Barotrauma.Items.Components } if (shoot) { - TryLaunch(deltaTime, ignorePower: true); + TryLaunch(deltaTime, ignorePower: ignorePower); } } @@ -1149,12 +1190,12 @@ namespace Barotrauma.Items.Components bool canShoot = HasPowerToShoot(); if (!canShoot) { - List batteries = GetDirectlyConnectedBatteries(); float lowestCharge = 0.0f; PowerContainer batteryToLoad = null; - foreach (PowerContainer battery in batteries) + foreach (PowerContainer battery in GetDirectlyConnectedBatteries()) { if (!battery.Item.IsInteractable(character)) { continue; } + if (battery.OutputDisabled) { continue; } if (batteryToLoad == null || battery.Charge < lowestCharge) { batteryToLoad = battery; @@ -1328,7 +1369,11 @@ namespace Barotrauma.Items.Components { // Only check the angle to targets that are close enough to be shot at // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. - if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(enemy.WorldPosition)) { continue; } + } + if (currentTarget != null && enemy == currentTarget) + { + priority *= GetTargetPriorityModifier(); } targetPos = enemy.WorldPosition; closestEnemy = enemy; @@ -1344,7 +1389,11 @@ namespace Barotrauma.Items.Components if (dist > closestDistance) { continue; } if (dist > shootDistance * shootDistance) { continue; } if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } - if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; } + if (currentTarget != null && targetItem == currentTarget) + { + priority *= GetTargetPriorityModifier(); + } targetPos = targetItem.WorldPosition; closestDistance = dist / priority; // Override the target character so that we can target the item instead. @@ -1378,7 +1427,7 @@ namespace Barotrauma.Items.Components { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } - if (!CheckTurretAngle(limb.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; } float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); if (dist < closestDist) { @@ -1416,14 +1465,14 @@ namespace Barotrauma.Items.Components Vector2 p1 = edge.Point1 + cell.Translation; Vector2 p2 = edge.Point2 + cell.Translation; Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); - if (!CheckTurretAngle(closestPoint)) + if (!IsWithinAimingRadius(closestPoint)) { // The closest point can't be targeted -> get a point directly in front of the turret Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { closestPoint = intersection; - if (!CheckTurretAngle(closestPoint)) { continue; } + if (!IsWithinAimingRadius(closestPoint)) { continue; } } else { @@ -1510,19 +1559,7 @@ namespace Barotrauma.Items.Components character.CursorPosition -= character.Submarine.Position; } - float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition); - float turretAngle = -rotation; - - float maxAngleError = 0.15f; - if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f) - { - //larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target) - maxAngleError *= 2.0f; - } - - if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; } - - if (canShoot) + if (IsPointingTowards(targetPos.Value)) { Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); @@ -1551,6 +1588,19 @@ namespace Barotrauma.Items.Components return false; } + private bool IsPointingTowards(Vector2 targetPos) + { + float enemyAngle = MathUtils.VectorToAngle(targetPos - item.WorldPosition); + float turretAngle = -rotation; + float maxAngleError = MathHelper.ToRadians(MaxAngleOffset); + if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f) + { + //larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target) + maxAngleError *= 2.0f; + } + return Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) <= maxAngleError; + } + private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance); /// @@ -1669,6 +1719,7 @@ namespace Barotrauma.Items.Components customPredicate: (Fixture f) => { if (f.UserData is Item i && i.GetComponent() != null) { return false; } + if (f.UserData is Hull) { return false; } return !item.StaticFixtures.Contains(f); }); return pickedBody; @@ -1686,7 +1737,7 @@ namespace Barotrauma.Items.Components return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y); } - private bool CheckTurretAngle(float angle) + private bool IsWithinAimingRadius(float angle) { float midRotation = (minRotation + maxRotation) / 2.0f; while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } @@ -1694,7 +1745,7 @@ namespace Barotrauma.Items.Components return angle >= minRotation && angle <= maxRotation; } - public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); + public bool IsWithinAimingRadius(Vector2 target) => IsWithinAimingRadius(-MathUtils.VectorToAngle(target - item.WorldPosition)); protected override void RemoveComponentSpecific() { @@ -1835,7 +1886,7 @@ namespace Barotrauma.Items.Components break; case "trigger_in": if (signal.value == "0") { return; } - item.Use((float)Timing.Step, sender); + item.Use((float)Timing.Step, user: sender); user = sender; ActiveUser = sender; resetActiveUserTimer = 1f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 92e4bc74c..c94cc73e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Abilities; +using System.Collections.Immutable; namespace Barotrauma { @@ -55,6 +56,11 @@ namespace Barotrauma public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide; public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip; public bool CanBeHiddenByOtherWearables { get; private set; } + + /// + /// Tags or identifiers of items that can hide this one. + /// + public ImmutableHashSet CanBeHiddenByItem { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } /// @@ -139,11 +145,6 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - ObscureOtherWearables = ObscuringMode.None; - InheritLimbDepth = true; - InheritScale = true; - InheritOrigin = true; - InheritSourceRect = true; break; } } @@ -177,6 +178,10 @@ namespace Barotrauma public void ParsePath(bool parseSpritePath) { +#if SERVER + // Server doesn't care about texture paths at all + return; +#endif SpritePath = UnassignedSpritePath.Value; if (_picker?.Info != null) { @@ -196,7 +201,7 @@ namespace Barotrauma } if (parseSpritePath) { - Sprite.ParseTexturePath(file: SpritePath); + Sprite?.ParseTexturePath(file: SpritePath); } } @@ -222,22 +227,23 @@ namespace Barotrauma } CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true); + CanBeHiddenByItem = SourceElement.GetAttributeIdentifierArray(nameof(CanBeHiddenByItem), Array.Empty()).ToImmutableHashSet(); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); var scale = SourceElement.GetAttribute("inheritscale"); if (scale != null) { - InheritScale = scale.GetAttributeBool(false); + InheritScale = scale.GetAttributeBool(Type != WearableType.Item); } else { - InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false); + InheritScale = SourceElement.GetAttributeBool("inherittexturescale", Type != WearableType.Item); } IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false); IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false); IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false); SourceElement.GetAttributeBool("inherittexturescale", false); - InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false); - InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false); + InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", Type != WearableType.Item); + InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", Type != WearableType.Item); DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); Sound = SourceElement.GetAttributeString("sound", ""); Scale = SourceElement.GetAttributeFloat("scale", 1.0f); @@ -457,22 +463,20 @@ namespace Barotrauma.Items.Components if (!equipLimb.WearingItems.Contains(wearableSprite)) { equipLimb.WearingItems.Add(wearableSprite); - equipLimb.WearingItems.Sort((i1, i2) => { return i2.Sprite.Depth.CompareTo(i1.Sprite.Depth); }); - equipLimb.WearingItems.Sort((i1, i2) => + equipLimb.WearingItems.Sort((wearable, nextWearable) => { - if (i1?.WearableComponent == null && i2?.WearableComponent == null) - { - return 0; - } - else if (i1?.WearableComponent == null) - { - return -1; - } - else if (i2?.WearableComponent == null) - { - return 1; - } - return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); + float depth = wearable?.Sprite?.Depth ?? 0; + float nextDepth = nextWearable?.Sprite?.Depth ?? 0; + return nextDepth.CompareTo(depth); + }); + equipLimb.WearingItems.Sort((wearable, nextWearable) => + { + var wearableComponent = wearable?.WearableComponent; + var nextWearableComponent = nextWearable?.WearableComponent; + if (wearableComponent == null && nextWearableComponent == null) { return 0; } + if (wearableComponent == null) { return -1; } + if (nextWearableComponent == null) { return 1; } + return wearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(nextWearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); }); } #if CLIENT @@ -553,10 +557,7 @@ namespace Barotrauma.Items.Components foreach (WearableSprite wearableSprite in wearableSprites) { - if (wearableSprite != null && wearableSprite.Sprite != null) - { - wearableSprite.Sprite.Remove(); - } + wearableSprite?.Sprite?.Remove(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 8be00ac66..af251d6ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -8,18 +8,27 @@ using System.Linq; namespace Barotrauma { - partial class Inventory : IServerSerializable, IClientSerializable + partial class Inventory { - public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63 + public const int MaxPossibleStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63 + + public const int MaxItemsPerNetworkEvent = 128; public class ItemSlot { - private readonly List items = new List(MaxStackSize); + private readonly List items = new List(MaxPossibleStackSize); public bool HideIfEmpty; public IReadOnlyList Items => items; + private readonly Inventory inventory; + + public ItemSlot(Inventory inventory) + { + this.inventory = inventory; + } + public bool CanBePut(Item item, bool ignoreCondition = false) { if (item == null) { return false; } @@ -41,7 +50,7 @@ namespace Barotrauma } } if (items[0].Quality != item.Quality) { return false; } - if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.MaxStackSize) + if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.GetMaxStackSize(inventory)) { return false; } @@ -80,7 +89,7 @@ namespace Barotrauma } if (items[0].Prefab.Identifier != itemPrefab.Identifier || - items.Count + 1 > itemPrefab.MaxStackSize) + items.Count + 1 > itemPrefab.GetMaxStackSize(inventory)) { return false; } @@ -92,7 +101,7 @@ namespace Barotrauma public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null) { if (itemPrefab == null) { return 0; } - maxStackSize ??= itemPrefab.MaxStackSize; + maxStackSize ??= itemPrefab.GetMaxStackSize(inventory); if (items.Count > 0) { if (condition.HasValue) @@ -135,9 +144,9 @@ namespace Barotrauma { throw new InvalidOperationException("Tried to stack different types of items."); } - else if (items.Count + 1 > item.Prefab.MaxStackSize) + else if (items.Count + 1 > item.Prefab.GetMaxStackSize(inventory)) { - throw new InvalidOperationException("Tried to add an item to a full inventory slot (stack already full)."); + throw new InvalidOperationException($"Tried to add an item to a full inventory slot (stack already full, x{items.Count} {items.First().Prefab.Identifier})."); } } if (items.Contains(item)) { return; } @@ -229,34 +238,11 @@ namespace Barotrauma /// /// 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. /// - public IEnumerable AllItems + public virtual IEnumerable AllItems { get { - for (int i = 0; i < capacity; i++) - { - foreach (var item in slots[i].Items) - { - if (item == null) - { -#if DEBUG - DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); -#endif - continue; - } - - bool duplicateFound = false; - for (int j = 0; j < i; j++) - { - if (slots[j].Items.Contains(item)) - { - duplicateFound = true; - break; - } - } - if (!duplicateFound) { yield return item; } - } - } + return GetAllItems(checkForDuplicates: this is CharacterInventory); } } @@ -292,7 +278,7 @@ namespace Barotrauma slots = new ItemSlot[capacity]; for (int i = 0; i < capacity; i++) { - slots[i] = new ItemSlot(); + slots[i] = new ItemSlot(this); } #if CLIENT @@ -315,6 +301,60 @@ namespace Barotrauma #endif } + protected IEnumerable GetAllItems(bool checkForDuplicates) + { + for (int i = 0; i < capacity; i++) + { + foreach (var item in slots[i].Items) + { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); +#endif + continue; + } + + if (checkForDuplicates) + { + bool duplicateFound = false; + for (int j = 0; j < i; j++) + { + if (slots[j].Items.Contains(item)) + { + duplicateFound = true; + break; + } + } + if (!duplicateFound) { yield return item; } + } + else + { + yield return item; + } + } + } + } + + private void NotifyItemComponentsOfChange() + { +#if CLIENT + if (Owner is Character character && character == Character.Controlled) + { + character.SelectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); + } +#endif + + if (Owner is not Item it) { return; } + + foreach (var c in it.Components) + { + c.OnInventoryChanged(); + } + + it.ParentInventory?.NotifyItemComponentsOfChange(); + } + /// /// Is the item contained in this inventory. Does not recursively check items inside items. /// @@ -367,6 +407,13 @@ namespace Barotrauma return slots[index].Items; } + public int GetItemStackSlotIndex(Item item, int index) + { + if (index < 0 || index >= slots.Length) { return -1; } + + return slots[index].Items.IndexOf(item); + } + /// /// Find the index of the first slot the item is contained in. /// @@ -534,7 +581,7 @@ namespace Barotrauma var itemInSlot = slots[i].First(); if (itemInSlot.OwnInventory != null && !itemInSlot.OwnInventory.Contains(item) && - (itemInSlot.GetComponent()?.GetMaxStackSize(0) ?? 0) == 1 && + itemInSlot.GetComponent()?.GetMaxStackSize(0) == 1 && itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false)) { return true; @@ -575,7 +622,7 @@ namespace Barotrauma if (removeItem) { - item.Drop(user, setTransform: false); + item.Drop(user, setTransform: false, createNetworkEvent: createNetworkEvent && GameMain.NetworkMember is { IsServer: true }); item.ParentInventory?.RemoveItem(item); } @@ -597,7 +644,7 @@ namespace Barotrauma item.body.BodyType = FarseerPhysics.BodyType.Dynamic; item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false); //update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies) - item.body.Update(); + item.body.UpdateDrawPosition(interpolate: false); } #if SERVER @@ -624,6 +671,8 @@ namespace Barotrauma } } } + + NotifyItemComponentsOfChange(); } public bool IsEmpty() @@ -648,7 +697,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; } + if (slots[i].Items.Count < item.Prefab.GetMaxStackSize(this)) { return false; } } } else @@ -878,21 +927,35 @@ namespace Barotrauma } } - public virtual void CreateNetworkEvent() + public void CreateNetworkEvent() { if (GameMain.NetworkMember == null) { return; } if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - if (Owner is Character character) + //split into multiple events because one might not be enough to fit all the items + List slotRanges = new List(); + int startIndex = 0; + int itemCount = 0; + for (int i = 0; i < capacity; i++) { - GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData()); + int count = slots[i].Items.Count; + if (itemCount + count > MaxItemsPerNetworkEvent || i == capacity - 1) + { + slotRanges.Add(new Range(startIndex, i + 1)); + startIndex = i + 1; + itemCount = 0; + } + itemCount += count; } - else if (Owner is Item item) + + foreach (var slotRange in slotRanges) { - GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData()); + CreateNetworkEvent(slotRange); } } + protected virtual void CreateNetworkEvent(Range slotRange) { } + public Item FindItem(Func predicate, bool recursive) { Item match = AllItems.FirstOrDefault(predicate); @@ -955,9 +1018,12 @@ namespace Barotrauma visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f); if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } + syncItemsDelay = 1.0f; #endif CharacterHUD.RecreateHudTextsIfFocused(item); } + + NotifyItemComponentsOfChange(); } /// @@ -989,28 +1055,48 @@ namespace Barotrauma return slots[index].Contains(item); } - public void SharedRead(IReadMessage msg, out List[] newItemIds) + public void SharedRead(IReadMessage msg, List[] receivedItemIds, out bool readyToApply) { - byte slotCount = msg.ReadByte(); - newItemIds = new List[slotCount]; - for (int i = 0; i < slotCount; i++) + byte start = msg.ReadByte(); + byte end = msg.ReadByte(); + + //if we received the first chunk of item IDs, clear the rest + //to ensure we don't have anything outdated in the list - we're about to receive the rest of the IDs next + if (start == 0) { - newItemIds[i] = new List(); - int itemCount = msg.ReadRangedInteger(0, MaxStackSize); - for (int j = 0; j < itemCount; j++) + for (int i = 0; i < capacity; i++) { - newItemIds[i].Add(msg.ReadUInt16()); + receivedItemIds[i] = null; } } + + for (int i = start; i < end; i++) + { + var newItemIds = new List(); + int itemCount = msg.ReadRangedInteger(0, MaxPossibleStackSize); + for (int j = 0; j < itemCount; j++) + { + newItemIds.Add(msg.ReadUInt16()); + } + receivedItemIds[i] = newItemIds; + } + + //if all IDs haven't been received yet (chunked into multiple events?) + //don't apply the state yet + readyToApply = !receivedItemIds.Contains(null); } - public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) + public void SharedWrite(IWriteMessage msg, Range slotRange) { - msg.WriteByte((byte)capacity); - for (int i = 0; i < capacity; i++) + int start = slotRange.Start.Value; + int end = slotRange.End.Value; + + msg.WriteByte((byte)start); + msg.WriteByte((byte)end); + for (int i = start; i < end; i++) { - msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize); - for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++) + msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxPossibleStackSize); + for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxPossibleStackSize); j++) { var item = slots[i].Items[j]; msg.WriteUInt16(item?.ID ?? (ushort)0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index dcdedaa7d..53495afc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -23,7 +23,7 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { - public static List ItemList = new List(); + public static readonly List ItemList = new List(); private static readonly HashSet dangerousItems = new HashSet(); @@ -77,17 +77,17 @@ namespace Barotrauma public CampaignMode.InteractionType CampaignInteractionType { get { return campaignInteractionType; } - set - { - if (campaignInteractionType != value) - { - campaignInteractionType = value; - AssignCampaignInteractionTypeProjSpecific(campaignInteractionType); - } - } } - partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType); + public void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable targetClients = null) + { + if (campaignInteractionType == interactionType) { return; } + campaignInteractionType = interactionType; + AssignCampaignInteractionTypeProjSpecific(campaignInteractionType, targetClients); + } + + + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients); public bool Visible = true; @@ -105,6 +105,12 @@ namespace Barotrauma private readonly List drawableComponents; private bool hasComponentsToDraw; + /// + /// Has everything in the item been loaded/instantiated/initialized (basically, can be used to check if the whole constructor/Load method has run). + /// Most commonly used to avoid creating network events when some value changes if the item is being initialized. + /// + public bool FullyInitialized { get; private set; } + public PhysicsBody body; private readonly float originalWaterDragCoefficient; private float? overrideWaterDragCoefficient; @@ -114,6 +120,21 @@ namespace Barotrauma set => overrideWaterDragCoefficient = value; } + /// + /// Can be used by StatusEffects to set the type of the body (if the item has one) + /// + public BodyType BodyType + { + get { return body?.BodyType ?? BodyType.Dynamic; } + set + { + if (body != null) + { + body.BodyType = value; + } + } + } + /// /// Removes the override value -> falls back to using the original value defined in the xml. /// @@ -125,9 +146,10 @@ namespace Barotrauma private bool transformDirty = true; + private static readonly List itemsWithPendingConditionUpdates = new List(); + private float lastSentCondition; private float sendConditionUpdateTimer; - private bool conditionUpdatePending; private float prevCondition; private float condition; @@ -207,7 +229,11 @@ namespace Barotrauma set { parentInventory = value; - if (parentInventory != null) { Container = parentInventory.Owner as Item; } + if (parentInventory != null) + { + Container = parentInventory.Owner as Item; + RemoveFromDroppedStack(allowClientExecute: false); + } #if SERVER PreviousParentInventory = value; #endif @@ -229,7 +255,6 @@ namespace Barotrauma container = value; CheckCleanable(); SetActiveSprite(); - RefreshRootContainer(); } } @@ -247,6 +272,37 @@ namespace Barotrauma set { description = value; } } + private string descriptionTag; + + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork)] + /// + /// Can be used to set a localized description via StatusEffects + /// + public string DescriptionTag + { + get { return descriptionTag; } + set + { + if (value == descriptionTag) { return; } + if (value.IsNullOrEmpty()) + { + descriptionTag = null; + description = null; + } + else + { + description = TextManager.Get(value).Value; + descriptionTag = value; + } + if (FullyInitialized && + SerializableProperties != null && + SerializableProperties.TryGetValue(nameof(DescriptionTag).ToIdentifier(), out SerializableProperty property)) + { + GameMain.NetworkMember?.CreateEntityEvent(this, new ChangePropertyEventData(property, this)); + } + } + } + [Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool NonInteractable { @@ -675,6 +731,8 @@ namespace Barotrauma set { allowStealing = value; } } + public bool IsSalvageMissionItem; + private string originalOutpost; [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public string OriginalOutpost @@ -875,8 +933,8 @@ namespace Barotrauma { get { return allPropertyObjects; } } - - public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; + + public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } public bool HasBallastFloraInHull @@ -896,6 +954,9 @@ namespace Barotrauma } } + public bool InPlayerSubmarine => Submarine?.Info is { IsPlayer: true }; + public bool InBeaconStation => Submarine?.Info is { Type: SubmarineType.BeaconStation }; + public bool IsLadder { get; } public bool IsSecondaryItem { get; } @@ -910,6 +971,11 @@ namespace Barotrauma } } + /// + /// Timing.TotalTimeUnpaused when some character was last eating the item + /// + public float LastEatenTime { get; set; } + public Action OnDeselect; public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) @@ -1157,10 +1223,15 @@ namespace Barotrauma DebugConsole.Log("Created " + Name + " (" + ID + ")"); if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } - if (HasTag("logic")) { isLogic = true; } + if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; } ApplyStatusEffects(ActionType.OnSpawn, 1.0f); RecalculateConditionValues(); + + if (callOnItemLoaded) + { + FullyInitialized = true; + } #if CLIENT Submarine.ForceVisibilityRecheck(); #endif @@ -1179,7 +1250,7 @@ namespace Barotrauma }; foreach (KeyValuePair property in SerializableProperties) { - if (!property.Value.Attributes.OfType().Any()) continue; + if (property.Value.Attributes.OfType().None()) { continue; } clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); } @@ -1194,9 +1265,10 @@ namespace Barotrauma for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - foreach (KeyValuePair property in components[i].SerializableProperties) + //order the properties to get them to be applied in a consistent order (may matter for properties that are interconnected somehow, like IsOn/IsActive) + foreach (KeyValuePair property in components[i].SerializableProperties.OrderBy(s => s.Key)) { - if (!property.Value.Attributes.OfType().Any()) continue; + if (property.Value.Attributes.OfType().None()) { continue; } clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); } @@ -1219,18 +1291,48 @@ namespace Barotrauma if (FlippedX) clone.FlipX(false); if (FlippedY) clone.FlipY(false); - + foreach (ItemComponent component in clone.components) { component.OnItemLoaded(); } - foreach (Item containedItem in ContainedItems) + Dictionary clonedContainedItems = new(); + + for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - var containedClone = containedItem.Clone(); - clone.ownInventory.TryPutItem(containedClone as Item, null); + ItemComponent component = components[i], + cloneComp = clone.components[i]; + + if (component is not ItemContainer origInv || + cloneComp is not ItemContainer cloneInv) + { + continue; + } + + foreach (var containedItem in origInv.Inventory.AllItems) + { + var containedClone = (Item)containedItem.Clone(); + + cloneInv.Inventory.TryPutItem(containedClone, null); + clonedContainedItems.Add(containedItem.ID, containedClone); + } } - + + for (int i = 0; i < components.Count && i < clone.components.Count; i++) + { + ItemComponent component = components[i], + cloneComp = clone.components[i]; + + if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox) + { + continue; + } + + cloneBox.CloneFrom(origBox, clonedContainedItems); + } + + clone.FullyInitialized = true; return clone; } @@ -1432,7 +1534,7 @@ namespace Barotrauma var pickable = GetComponent(); if (pickable != null && !pickable.IsAttached && Prefab.PreferredContainers.Any() && - (container == null || container.HasTag("allowcleanup"))) + (container == null || container.HasTag(Barotrauma.Tags.AllowCleanup))) { if (!cleanableItems.Contains(this)) { @@ -1638,12 +1740,6 @@ namespace Barotrauma tags.Add(tag); } - - public bool HasTag(string tag) - { - return HasTag(tag.ToIdentifier()); - } - public bool HasTag(Identifier tag) { if (tag == null) { return true; } @@ -1690,7 +1786,7 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { - if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (string.IsNullOrEmpty(conditional.TargetItemComponent)) { if (!conditional.Matches(this)) { return false; } } @@ -1698,7 +1794,7 @@ namespace Barotrauma { foreach (ItemComponent component in components) { - if (component.Name != conditional.TargetItemComponentName) { continue; } + if (component.Name != conditional.TargetItemComponent) { continue; } if (!conditional.Matches(component)) { return false; } } } @@ -1796,7 +1892,7 @@ namespace Barotrauma } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - targets.AddRange(character.AnimController.Limbs.ToList()); + targets.AddRange(character.AnimController.Limbs); } if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null) { @@ -1811,7 +1907,7 @@ namespace Barotrauma targets.Add(limb); } - if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); } + if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.AddRange(Container.AllPropertyObjects); } effect.Apply(type, deltaTime, this, targets, worldPosition); } @@ -1897,21 +1993,20 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - if (Math.Abs(lastSentCondition - condition) > 1.0f) - { - conditionUpdatePending = true; - isActive = true; - } - else if (wasInFullCondition != IsFullCondition) - { - conditionUpdatePending = true; - isActive = true; - } - else if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition)) + bool needsConditionUpdate = false; + if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition)) { + //send the update immediately if the condition changed to max or min sendConditionUpdateTimer = 0.0f; - conditionUpdatePending = true; - isActive = true; + needsConditionUpdate = true; + } + else if (Math.Abs(lastSentCondition - condition) > 1.0f || wasInFullCondition != IsFullCondition) + { + needsConditionUpdate = true; + } + if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this)) + { + itemsWithPendingConditionUpdates.Add(this); } } @@ -1967,12 +2062,16 @@ namespace Barotrauma public void SendPendingNetworkUpdates() { if (!(GameMain.NetworkMember is { IsServer: true })) { return; } - if (!conditionUpdatePending) { return; } + if (!itemsWithPendingConditionUpdates.Contains(this)) { return; } + SendPendingNetworkUpdatesInternal(); + itemsWithPendingConditionUpdates.Remove(this); + } + private void SendPendingNetworkUpdatesInternal() + { CreateStatusEvent(loadingRound: false); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; - conditionUpdatePending = false; } public void CreateStatusEvent(bool loadingRound) @@ -1980,21 +2079,32 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound)); } + public static void UpdatePendingConditionUpdates(float deltaTime) + { + if (GameMain.NetworkMember is not { IsServer: true }) { return; } + for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++) + { + var item = itemsWithPendingConditionUpdates[i]; + if (item == null || item.Removed) + { + itemsWithPendingConditionUpdates.RemoveAt(i--); + continue; + } + if (item.Submarine is { Loading: true }) { continue; } + + item.sendConditionUpdateTimer -= deltaTime; + if (item.sendConditionUpdateTimer <= 0.0f) + { + item.SendPendingNetworkUpdatesInternal(); + itemsWithPendingConditionUpdates.RemoveAt(i--); + } + } + } + private bool isActive = true; public override void Update(float deltaTime, Camera cam) { -#if SERVER - if (!(Submarine is { Loading: true })) - { - sendConditionUpdateTimer -= deltaTime; - if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) - { - SendPendingNetworkUpdates(); - } - } -#endif - if (!isActive) { return; } if (impactQueue != null) @@ -2004,14 +2114,27 @@ namespace Barotrauma HandleCollision(impact); } } + if (isDroppedStackOwner && body != null) + { + foreach (var item in droppedStack) + { + if (item != this) + { + item.body.Enabled = false; + item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation); + } + } + } if (aiTarget != null && aiTarget.NeedsUpdate) { aiTarget.Update(deltaTime); } + var containedEffectType = parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained; + ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); - ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); + ApplyStatusEffects(containedEffectType, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); for (int i = 0; i < updateableComponents.Count; i++) { @@ -2019,7 +2142,7 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) + if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) { bool shouldBeActive = true; foreach (var conditional in ic.IsActiveConditionals) @@ -2140,6 +2263,7 @@ namespace Barotrauma updateableComponents.Count == 0 && (aiTarget == null || !aiTarget.NeedsUpdate) && !hasStatusEffectsOfType[(int)ActionType.Always] && + !hasStatusEffectsOfType[(int)containedEffectType] && (body == null || !body.Enabled)) { #if CLIENT @@ -2651,9 +2775,20 @@ namespace Barotrauma public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false) { - if (CampaignMode.BlocksInteraction(CampaignInteractionType)) + var campaignInteractionType = CampaignInteractionType; +#if SERVER + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == user); + if (ownerClient != null) { - return false; + if (!campaignInteractionTypePerClient.TryGetValue(ownerClient, out campaignInteractionType)) + { + campaignInteractionType = CampaignMode.InteractionType.None; + } + } +#endif + if (CampaignMode.BlocksInteraction(campaignInteractionType)) + { + return false; } bool picked = false, selected = false; @@ -2821,9 +2956,9 @@ namespace Barotrauma return -1; } - public void Use(float deltaTime, Character character = null, Limb targetLimb = null) + public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null) { - if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim))) + if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim))) { return; } @@ -2836,17 +2971,17 @@ namespace Barotrauma { bool isControlled = false; #if CLIENT - isControlled = character == Character.Controlled; + isControlled = user == Character.Controlled; #endif - if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; } - if (ic.Use(deltaTime, character)) + if (!ic.HasRequiredContainedItems(user, isControlled)) { continue; } + if (ic.Use(deltaTime, user)) { ic.WasUsed = true; #if CLIENT - ic.PlaySound(ActionType.OnUse, character); + ic.PlaySound(ActionType.OnUse, user); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character); - + ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user); + ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user)); if (ic.DeleteOnUse) { remove = true; } } } @@ -2876,7 +3011,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character); + ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character, useTarget: character); if (ic.DeleteOnUse) { remove = true; } } @@ -3009,10 +3144,10 @@ namespace Barotrauma Container = null; } - if (parentInventory != null) + if (ParentInventory != null) { - parentInventory.RemoveItem(this); - parentInventory = null; + ParentInventory.RemoveItem(this); + ParentInventory = null; } SetContainedItemPositions(); @@ -3021,6 +3156,108 @@ namespace Barotrauma #endif } + + private List droppedStack; + public IEnumerable DroppedStack => droppedStack ?? Enumerable.Empty(); + + private bool isDroppedStackOwner; + + /// + /// "Merges" the set of items so they behave as one physical object and can be picked up by clicking once. + /// The items need to be instances of the same prefab and have a physics body. + /// + public void CreateDroppedStack(IEnumerable items, bool allowClientExecute) + { + if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; } + + int itemCount = items.Count(); + + if (itemCount == 1) { return; } + + if (items.DistinctBy(it => it.Prefab).Count() > 1) + { + DebugConsole.ThrowError($"Attempted to create a dropped stack of multiple different items ({string.Join(", ", items.DistinctBy(it => it.Prefab))})\n{Environment.StackTrace}"); + return; + } + if (items.Any(it => it.body == null)) + { + DebugConsole.ThrowError($"Attempted to create a dropped stack for an item with no body ({items.First().Prefab.Identifier})\n{Environment.StackTrace}"); + return; + } + if (items.None()) + { + DebugConsole.ThrowError($"Attempted to create a dropped stack of an empty list of items.\n{Environment.StackTrace}"); + return; + } + + int maxStackSize = items.First().Prefab.MaxStackSize; + if (itemCount > maxStackSize) + { + for (int i = 0; i < MathF.Ceiling(itemCount / maxStackSize); i++) + { + int startIndex = i * maxStackSize; + items.ElementAt(startIndex).CreateDroppedStack(items.Skip(startIndex).Take(maxStackSize), allowClientExecute); + } + } + else + { + droppedStack ??= new List(); + foreach (Item item in items) + { + if (!droppedStack.Contains(item)) + { + droppedStack.Add(item); + } + } + SetDroppedStackItemStates(); +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(this, new DroppedStackEventData(droppedStack)); + } +#endif + } + } + + private void RemoveFromDroppedStack(bool allowClientExecute) + { + if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; } + if (droppedStack == null) { return; } + + body.Enabled = ParentInventory == null; + isDroppedStackOwner = false; + droppedStack.Remove(this); + SetDroppedStackItemStates(); + droppedStack = null; +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(this, new DroppedStackEventData(Enumerable.Empty())); + } +#endif + } + + private void SetDroppedStackItemStates() + { + if (droppedStack == null) { return; } + bool isFirst = true; + foreach (Item item in droppedStack) + { + item.droppedStack = droppedStack; + item.isDroppedStackOwner = isFirst; + if (item.body != null) + { + item.body.Enabled = item.body.PhysEnabled = isFirst; + if (isFirst) + { + item.isActive = true; + item.body.ResetDynamics(); + } + } + isFirst = false; + } + } + public void Equip(Character character) { if (Removed) @@ -3072,7 +3309,7 @@ namespace Barotrauma if (allProperties.Count > 1) { int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity); - if (propertyIndex < -1) + if (propertyIndex < 0) { throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\""); } @@ -3187,6 +3424,11 @@ namespace Barotrauma propertyIndex = (int)msg.ReadVariableUInt32(); } + if (propertyIndex >= allProperties.Count || propertyIndex < 0) + { + throw new Exception($"Error in ReadPropertyChange. Property index out of bounds (index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})"); + } + bool allowEditing = true; object parentObject = allProperties[propertyIndex].obj; SerializableProperty property = allProperties[propertyIndex].property; @@ -3598,7 +3840,9 @@ namespace Barotrauma if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } - + + item.FullyInitialized = true; + return item; } @@ -3811,6 +4055,7 @@ namespace Barotrauma repairableItems.Remove(this); sonarVisibleItems.Remove(this); cleanableItems.Remove(this); + RemoveFromDroppedStack(allowClientExecute: true); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 010a675b2..fecfef994 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -20,9 +22,10 @@ namespace Barotrauma ApplyStatusEffect = 7, Upgrade = 8, ItemStat = 9, - + DroppedStack = 10, + MinValue = 0, - MaxValue = 9 + MaxValue = 10 } public interface IEventData : NetEntityEvent.IData @@ -47,10 +50,12 @@ namespace Barotrauma { public EventType EventType => EventType.InventoryState; public readonly ItemContainer Component; + public readonly Range SlotRange; - public InventoryStateEventData(ItemContainer component) + public InventoryStateEventData(ItemContainer component, Range slotRange) { Component = component; + SlotRange = slotRange; } } @@ -62,6 +67,10 @@ namespace Barotrauma public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity) { + if (serializableProperty.GetAttribute() == null) + { + DebugConsole.ThrowError($"Attempted to create {nameof(ChangePropertyEventData)} for the non-editable property {serializableProperty.Name}."); + } SerializableProperty = serializableProperty; Entity = entity; } @@ -94,6 +103,13 @@ namespace Barotrauma private readonly struct AssignCampaignInteractionEventData : IEventData { public EventType EventType => EventType.AssignCampaignInteraction; + + public readonly ImmutableArray TargetClients; + + public AssignCampaignInteractionEventData(IEnumerable targetClients) + { + TargetClients = (targetClients ?? Enumerable.Empty()).ToImmutableArray(); + } } public readonly struct ApplyStatusEffectEventData : IEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index a25062e49..0b8aea197 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -63,7 +63,7 @@ namespace Barotrauma if (itemPrefab == null) { return 0; } if (i < 0 || i >= slots.Length) { return 0; } if (!container.CanBeContained(itemPrefab, i)) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.GetMaxStackSize(i)), condition); + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition); } public override bool IsFull(bool takeStacksIntoAccount = false) @@ -74,7 +74,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } + if (slots[i].Items.Count < Math.Min(item.Prefab.GetMaxStackSize(this), container.GetMaxStackSize(i))) { return false; } } } else @@ -133,7 +133,7 @@ namespace Barotrauma return wasPut; } - public override void CreateNetworkEvent() + protected override void CreateNetworkEvent(Range slotRange) { if (!Item.ItemList.Contains(container.Item)) { @@ -150,11 +150,17 @@ namespace Barotrauma DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components"); return; } - + + if (slotRange.Start.Value < 0 || slotRange.End.Value > capacity) + { + DebugConsole.ThrowError($"Error when creating an inventory event: invalid slot range ({slotRange})\n" + Environment.StackTrace); + return; + } + if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container)); + GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container, slotRange)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index af2e67a85..27e58824a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -43,9 +43,9 @@ namespace Barotrauma public readonly bool CopyCondition; //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this - public readonly string[] RequiredDeconstructor; + public readonly Identifier[] RequiredDeconstructor; //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this - public readonly string[] RequiredOtherItem; + public readonly Identifier[] RequiredOtherItem; //text to display on the deconstructor's activate button when this output is available public readonly string ActivateButtonText; public readonly string InfoText; @@ -63,9 +63,9 @@ namespace Barotrauma OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); - RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", - element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); - RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty()); + RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor", + element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); + RequiredOtherItem = element.GetAttributeIdentifierArray("requiredotheritem", Array.Empty()); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); @@ -88,20 +88,27 @@ namespace Barotrauma public abstract ItemPrefab FirstMatchingPrefab { get; } + public LocalizedString OverrideHeader { get; } public LocalizedString OverrideDescription { get; } - public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) + public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem) { Amount = amount; MinCondition = minCondition; MaxCondition = maxCondition; UseCondition = useCondition; + OverrideHeader = overrideHeader; OverrideDescription = overrideDescription; + DefaultItem = defaultItem; } public readonly int Amount; public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; + /// + /// Used only when there's multiple optional items. + /// + public readonly Identifier DefaultItem; public bool IsConditionSuitable(float conditionPercentage) { @@ -138,8 +145,8 @@ namespace Barotrauma return item?.Prefab.Identifier == ItemPrefabIdentifier; } - public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) : - base(amount, minCondition, maxCondition, useCondition, overrideDescription) + public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) : + base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem: Identifier.Empty) { ItemPrefabIdentifier = itemPrefab; using MD5 md5 = MD5.Create(); @@ -158,9 +165,26 @@ namespace Barotrauma public override UInt32 UintIdentifier { get; } - public override IEnumerable ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)); + private readonly List cachedPrefabs = new List(); - public override ItemPrefab FirstMatchingPrefab => ItemPrefab.Prefabs.FirstOrDefault(p => p.Tags.Contains(Tag)); + private Md5Hash prevContentPackagesHash; + + public override IEnumerable ItemPrefabs + { + get + { + if (prevContentPackagesHash == null || + !prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash)) + { + cachedPrefabs.Clear(); + cachedPrefabs.AddRange(ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag))); + prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash; + } + return cachedPrefabs; + } + } + + public override ItemPrefab FirstMatchingPrefab => ItemPrefabs.FirstOrDefault(); public override bool MatchesItem(Item item) { @@ -168,8 +192,8 @@ namespace Barotrauma return item.HasTag(Tag); } - public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) - : base(amount, minCondition, maxCondition, useCondition, overrideDescription) + public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem) + : base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem) { Tag = tag; using MD5 md5 = MD5.Create(); @@ -186,7 +210,7 @@ namespace Barotrauma public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; private readonly Lazy displayName; - public LocalizedString DisplayName + public LocalizedString DisplayName => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; public readonly ImmutableArray RequiredItems; public readonly ImmutableArray SuitableFabricatorIdentifiers; @@ -198,6 +222,7 @@ namespace Barotrauma public readonly uint RecipeHash; public readonly int Amount; public readonly int? Quality; + public readonly bool HideForNonTraitors; /// /// How many of this item the fabricator can create (< 0 = unlimited) @@ -230,6 +255,8 @@ namespace Barotrauma FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault); FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault); + HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false); + if (element.GetAttribute(nameof(Quality)) != null) { Quality = element.GetAttributeInt(nameof(Quality), 0); @@ -266,10 +293,15 @@ namespace Barotrauma bool useCondition = subElement.GetAttributeBool("usecondition", true); int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); - LocalizedString description = string.Empty; - if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty()) + LocalizedString overrideDescription = string.Empty; + if (subElement.GetAttributeString("description", string.Empty) is string descriptionTag && !descriptionTag.IsNullOrEmpty()) { - description = TextManager.Get(texTag); + overrideDescription = TextManager.Get(descriptionTag); + } + LocalizedString overrideHeader = string.Empty; + if (subElement.GetAttributeString("header", string.Empty) is string headerTag && !headerTag.IsNullOrEmpty()) + { + overrideHeader = TextManager.Get(headerTag); } if (requiredItemIdentifier != Identifier.Empty) @@ -284,7 +316,7 @@ namespace Barotrauma amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } - requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description)); + requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader)); } else { @@ -298,7 +330,8 @@ namespace Barotrauma amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } - requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description)); + Identifier defaultItem = subElement.GetAttributeIdentifier("defaultitem", Identifier.Empty); + requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem)); } break; } @@ -316,12 +349,13 @@ namespace Barotrauma uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); var requiredItems = string.Join(':', RequiredItems - .Select(i => i.UintIdentifier) - .Select(i => string.Join(',', i))); + .Select(static i => $"{i.UintIdentifier}:{i.Amount}") + .Select(static i => string.Join(',', i))); + // above but include the item amount var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}")); - uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5); + uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5); if (retVal == 0) { retVal = 1; } return retVal; } @@ -681,6 +715,9 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.No)] public float OffsetOnSelected { get; private set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the character who's selected the item grab it (hold their hand on it, the same way as they do when repairing)? Defaults to true on items that have an ItemContainer component.")] + public bool GrabWhenSelected { get; set; } + private float health; [Serialize(100.0f, IsPropertySaveable.No)] @@ -749,7 +786,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DisableItemUsageWhenSelected { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("metalcrate", IsPropertySaveable.No)] public string CargoContainerIdentifier { get; private set; } [Serialize(false, IsPropertySaveable.No)] @@ -802,7 +839,40 @@ namespace Barotrauma public int MaxStackSize { get { return maxStackSize; } - private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } + private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxPossibleStackSize); } + } + + private int maxStackSizeCharacterInventory; + [Serialize(-1, IsPropertySaveable.No)] + public int MaxStackSizeCharacterInventory + { + get { return maxStackSizeCharacterInventory; } + private set { maxStackSizeCharacterInventory = Math.Min(value, Inventory.MaxPossibleStackSize); } + } + + private int maxStackSizeHoldableOrWearableInventory; + [Serialize(-1, IsPropertySaveable.No)] + public int MaxStackSizeHoldableOrWearableInventory + { + get { return maxStackSizeHoldableOrWearableInventory; } + private set { maxStackSizeHoldableOrWearableInventory = Math.Min(value, Inventory.MaxPossibleStackSize); } + } + + public int GetMaxStackSize(Inventory inventory) + { + if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0) + { + return maxStackSizeCharacterInventory; + } + else if (maxStackSizeHoldableOrWearableInventory > 0 && + inventory?.Owner is Item item && (item.GetComponent() != null || item.GetComponent() != null)) + { + return maxStackSizeHoldableOrWearableInventory; + } + else + { + return maxStackSize; + } } [Serialize(false, IsPropertySaveable.No)] @@ -830,13 +900,19 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] public float AISlowTurretPriority { get; private set; } - + [Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")] public float AITurretTargetingMaxDistance { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")] public bool AllowStealingContainedItems { get; private set; } + [Serialize("255,255,255,255", IsPropertySaveable.No, description: "Used in circuit box to set the color of the nodes.")] + public Color SignalComponentColor { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the player is unable to open the middle click menu when this item is selected.")] + public bool DisableCommandMenuWhenSelected { get; set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -1040,7 +1116,7 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in item prefab \"{ToString()}\": " + - $"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " + + $"{prevRecipe.TargetItemPrefabIdentifier} has the same hash as {newRecipe.TargetItemPrefabIdentifier}. " + $"This will cause issues with fabrication." ); } @@ -1172,6 +1248,11 @@ namespace Barotrauma #endif this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty()).ToImmutableHashSet(); + + GrabWhenSelected = ConfigElement.GetAttributeBool( + nameof(GrabWhenSelected), + ConfigElement.GetChildElement(nameof(ItemContainer)) != null && + ConfigElement.GetChildElement("Body") == null); } public CommonnessInfo? GetCommonnessInfo(Level level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index c4647ef25..4e65d9b65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -43,21 +43,24 @@ namespace Barotrauma } /// - /// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. + /// Should an empty inventory (or an empty inventory slot if is set) be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. /// public bool MatchOnEmpty { get; set; } /// - /// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. + /// Should only an empty inventory (or an empty inventory slot if is set) be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. /// public bool RequireEmpty { get; set; } + private bool RequireOrMatchOnEmpty => MatchOnEmpty || RequireEmpty; + /// /// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor, /// making it easier to for example make rewire things that require some special tool to rewire. /// public bool IgnoreInEditor { get; set; } + /// /// Identifier(s) or tag(s) of the items that are NOT considered valid. /// Can be used to, for example, exclude some specific items when using tags that apply to multiple items. @@ -103,6 +106,11 @@ namespace Barotrauma /// public int TargetSlot = -1; + /// + /// The slot type the target must be in when targeting an item contained inside a character's inventory + /// + public InvSlotType CharacterInventorySlotType; + /// /// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer. /// @@ -167,6 +175,10 @@ namespace Barotrauma { if (item.HasTag(excludedIdentifier)) { return false; } } + if (item.ParentInventory?.Owner is Character character && CharacterInventorySlotType != InvSlotType.None) + { + if (!character.HasEquippedItem(item, CharacterInventorySlotType)) { return false; } + } if (Identifiers.Contains(item.Prefab.Identifier)) { return true; } foreach (var identifier in Identifiers) { @@ -261,6 +273,8 @@ namespace Barotrauma Rotation = element.GetAttributeFloat("rotation", 0f); SetActive = element.GetAttributeBool("setactive", false); + CharacterInventorySlotType = element.GetAttributeEnum(nameof(CharacterInventorySlotType), InvSlotType.None); + if (element.GetAttribute(nameof(Hide)) != null) { Hide = element.GetAttributeBool(nameof(Hide), false); @@ -332,7 +346,7 @@ namespace Barotrauma case RelationType.Equipped: if (character == null) { return false; } var heldItems = character.HeldItems; - if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; } + if (RequireOrMatchOnEmpty && heldItems.None()) { return true; } foreach (Item equippedItem in heldItems) { if (equippedItem == null) { continue; } @@ -347,7 +361,7 @@ namespace Barotrauma if (character == null) { return false; } if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; } var allItems = character.Inventory.AllItems; - if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; } + if (RequireOrMatchOnEmpty && allItems.None()) { return true; } foreach (Item pickedItem in allItems) { if (pickedItem == null) { continue; } @@ -370,11 +384,21 @@ namespace Barotrauma private bool CheckContained(Item parentItem) { if (parentItem.OwnInventory == null) { return false; } - bool isEmpty = parentItem.OwnInventory.IsEmpty(); - if (RequireEmpty && !isEmpty) { return false; } - if (MatchOnEmpty && isEmpty) { return true; } + + if (TargetSlot == -1 && RequireOrMatchOnEmpty) + { + bool isEmpty = parentItem.OwnInventory.IsEmpty(); + if (RequireEmpty) { return isEmpty; } + if (MatchOnEmpty && isEmpty) { return true; } + } foreach (var container in parentItem.GetComponents()) { + if (TargetSlot > -1 && RequireOrMatchOnEmpty) + { + var itemInSlot = container.Inventory.GetItemAt(TargetSlot); + if (RequireEmpty) { return itemInSlot == null; } + if (MatchOnEmpty && itemInSlot == null) { return true; } + } foreach (Item contained in container.Inventory.AllItems) { if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } @@ -389,7 +413,8 @@ namespace Barotrauma { element.Add( new XAttribute("items", JoinedIdentifiers), - new XAttribute("type", type.ToString()), + new XAttribute("type", type.ToString()), + new XAttribute("characterinventoryslottype", CharacterInventorySlotType.ToString()), new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs index a8c23fb81..bafe8e15d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs @@ -9,11 +9,13 @@ namespace Barotrauma { public Identifier Item; public int Amount; - + public bool MultiPlayerOnly; + public StartItem(XElement element) { Item = element.GetAttributeIdentifier("identifier", Identifier.Empty); - Amount = element.GetAttributeInt("amount", 1); + Amount = element.GetAttributeInt(nameof(Amount), 1); + MultiPlayerOnly = element.GetAttributeBool(nameof(MultiPlayerOnly), false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 91797b6ee..dce56e175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -92,6 +92,11 @@ namespace Barotrauma /// private bool flash; + /// + /// Whether a debris particle effect is created when the explosion happens. + /// + private bool debris; + /// /// Whether a underwater bubble particle effect is created when the explosion happens. /// @@ -120,12 +125,15 @@ namespace Barotrauma /// /// List of item tags that the explosion ignores when applying fire effects. /// - private readonly string[] ignoreFireEffectsForTags; + private readonly Identifier[] ignoreFireEffectsForTags; /// /// When set to true, the explosion don't deal less damage when the target is behind a solid object. /// - private readonly bool ignoreCover; + public bool IgnoreCover + { + get; set; + } /// /// How long the light source created by the explosion lasts. @@ -185,11 +193,12 @@ namespace Barotrauma this.EmpStrength = empStrength; BallastFloraDamage = ballastFloraStrength; sparks = true; + debris = true; shockwave = true; smoke = true; flames = true; underwaterBubble = true; - ignoreFireEffectsForTags = Array.Empty(); + ignoreFireEffectsForTags = Array.Empty(); } public Explosion(ContentXElement element, string parentDebugName) @@ -205,13 +214,14 @@ namespace Barotrauma flames = element.GetAttributeBool("flames", showEffects); underwaterBubble = element.GetAttributeBool("underwaterbubble", showEffects); smoke = element.GetAttributeBool("smoke", showEffects); + debris = element.GetAttributeBool("debris", false); playTinnitus = element.GetAttributeBool("playtinnitus", showEffects); applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects); - ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty(), convertToLowerInvariant: true); + ignoreFireEffectsForTags = element.GetAttributeIdentifierArray("ignorefireeffectsfortags", Array.Empty()); - ignoreCover = element.GetAttributeBool("ignorecover", false); + IgnoreCover = element.GetAttributeBool("ignorecover", false); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); @@ -243,6 +253,7 @@ namespace Barotrauma shockwave = false; smoke = false; flash = false; + debris = false; flames = false; underwaterBubble = false; } @@ -470,7 +481,7 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion - if (!ignoreCover) + if (!IgnoreCover) { distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); } @@ -583,7 +594,6 @@ namespace Barotrauma } } - private static readonly List damagedStructureList = new List(); private static readonly Dictionary damagedStructures = new Dictionary(); /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken @@ -591,37 +601,30 @@ namespace Barotrauma public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, bool emitWallDamageParticles = true) { float dist = 600.0f; - damagedStructureList.Clear(); - foreach (MapEntity entity in MapEntity.mapEntityList) + damagedStructures.Clear(); + foreach (Structure structure in Structure.WallList) { - if (entity is not Structure structure) { continue; } - if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; } + if (ignoredSubmarines != null && structure.Submarine != null && ignoredSubmarines.Contains(structure.Submarine)) { continue; } if (structure.HasBody && !structure.IsPlatform && Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f) { - damagedStructureList.Add(structure); - } - } - - damagedStructures.Clear(); - foreach (Structure structure in damagedStructureList) - { - for (int i = 0; i < structure.SectionCount; i++) - { - float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); - if (distFactor <= 0.0f) { continue; } - - structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); - - if (damagedStructures.ContainsKey(structure)) + for (int i = 0; i < structure.SectionCount; i++) { - damagedStructures[structure] += damage * distFactor; - } - else - { - damagedStructures.Add(structure, damage * distFactor); + float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); + if (distFactor <= 0.0f) { continue; } + + structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); + + if (damagedStructures.ContainsKey(structure)) + { + damagedStructures[structure] += damage * distFactor; + } + else + { + damagedStructures.Add(structure, damage * distFactor); + } } } } @@ -717,7 +720,7 @@ namespace Barotrauma if (body.UserData is Item item) { var door = item.GetComponent(); - if (door != null && !door.IsBroken) { damageMultiplier *= 0.01f; } + if (door != null && !door.IsOpen && !door.IsBroken) { damageMultiplier *= 0.01f; } } else if (body.UserData is Structure structure) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 77b7a1e08..4c93da0cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -577,8 +577,8 @@ namespace Barotrauma } else { - hull1.LethalPressure = 0.0f; - hull2.LethalPressure = 0.0f; + hull1.LethalPressure -= Hull.PressureDropSpeed * deltaTime; + hull2.LethalPressure -= Hull.PressureDropSpeed * deltaTime; } } } @@ -642,7 +642,7 @@ namespace Barotrauma } else { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; } } else @@ -657,7 +657,7 @@ namespace Barotrauma } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 5d98d6a87..7de5bf629 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -8,6 +8,8 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Items.Components; +using Barotrauma.Extensions; namespace Barotrauma { @@ -140,6 +142,16 @@ namespace Barotrauma get { return properties; } } + /// + /// How fast the pressure in the hull builds up when there's a gap leading outside + /// + public const float PressureBuildUpSpeed = 15.0f; + + /// + /// How fast the pressure in the hull goes back to normal when it's no longer full of water + /// + public const float PressureDropSpeed = 10.0f; + private float lethalPressure; private float surface; @@ -310,6 +322,8 @@ namespace Barotrauma } } + public bool IsAirlock { get; private set; } + private bool ForceAsWetRoom => roomName != null && ( roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || @@ -404,6 +418,26 @@ namespace Barotrauma private bool networkUpdatePending; private float networkUpdateTimer; + /// + /// Average color of the background sections + /// + public Color AveragePaintedColor { get; private set; } + + /// + /// Returns true if the red component of the background color is twice as bright as the blue and green. Can be used by StatusEffects. + /// + public bool IsRed => ColorExtensions.IsRedDominant(AveragePaintedColor, minimumAlpha: 100); + + /// + /// Returns true if the green component of the background color is twice as bright as the red and blue. Can be used by StatusEffects. + /// + public bool IsGreen => ColorExtensions.IsGreenDominant(AveragePaintedColor, minimumAlpha: 100); + + /// + /// Returns true if the blue component of the background color is twice as bright as the red and green. Can be used by StatusEffects. + /// + public bool IsBlue => ColorExtensions.IsBlueDominant(AveragePaintedColor, minimumAlpha: 100); + public List FireSources { get; private set; } public List FakeFireSources { get; private set; } @@ -556,7 +590,9 @@ namespace Barotrauma } } Pressure = rect.Y - rect.Height + waterVolume / rect.Width; - + + DetermineIsAirlock(); + BallastFlora?.OnMapLoaded(); #if CLIENT lastAmbientLightEditTime = 0.0; @@ -980,7 +1016,7 @@ namespace Barotrauma if (waterVolume < Volume) { - LethalPressure -= 10.0f * deltaTime; + LethalPressure -= PressureDropSpeed * deltaTime; if (WaterVolume <= 0.0f) { #if CLIENT @@ -1264,7 +1300,7 @@ namespace Barotrauma { if (g.ConnectedDoor != null && !HullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true; } - List structures = mapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect)); + List structures = MapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect)); return structures.Any(st => !(st as Structure).CastShadow); } return false; @@ -1280,7 +1316,7 @@ namespace Barotrauma if (item.GetComponent() != null) roomItems.Add("engine"); if (item.GetComponent() != null) roomItems.Add("steering"); if (item.GetComponent() != null) roomItems.Add("sonar"); - if (item.HasTag("ballast")) roomItems.Add("ballast"); + if (item.HasTag(Tags.Ballast)) roomItems.Add("ballast"); } if (roomItems.Contains("reactor")) @@ -1297,7 +1333,7 @@ namespace Barotrauma if (moduleFlags != null && moduleFlags.Any() && (Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost)) { - if (moduleFlags.Contains("airlock".ToIdentifier()) && + if (moduleFlags.Contains(Tags.Airlock) && ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null)) { return "RoomName.Airlock"; @@ -1334,23 +1370,26 @@ namespace Barotrauma /// /// Is this hull or any of the items inside it tagged as "airlock"? /// - public bool IsTaggedAirlock() + private void DetermineIsAirlock() { if (RoomName != null && RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) { - return true; + IsAirlock = true; + return; } else { + var airlockTag = "airlock".ToIdentifier(); foreach (Item item in Item.ItemList) { - if (item.CurrentHull != this && item.HasTag("airlock")) + if (item.CurrentHull != this && item.HasTag(airlockTag)) { - return true; + IsAirlock = true; + return; } } } - return false; + IsAirlock = false; } /// @@ -1489,6 +1528,7 @@ namespace Barotrauma #endif sectionUpdated = true; } + RefreshAveragePaintedColor(); } if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate) @@ -1501,6 +1541,17 @@ namespace Barotrauma } } + private void RefreshAveragePaintedColor() + { + Vector4 avgColor = Vector4.Zero; + foreach (var anySection in BackgroundSections) + { + avgColor += anySection.Color.ToVector4(); + } + avgColor /= BackgroundSections.Count; + AveragePaintedColor = new Color(avgColor); + } + public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength) { if (color != null) @@ -1619,6 +1670,7 @@ namespace Barotrauma } } } + hull.RefreshAveragePaintedColor(); SerializableProperty.DeserializeProperties(hull, element); if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; } @@ -1687,7 +1739,7 @@ namespace Barotrauma public override string ToString() { - return $"{base.ToString()} ({Name ?? "unnamed"})"; + return $"{base.ToString()} ({DisplayName ?? "unnamed"}, {(Submarine?.Info?.Name ?? "no sub")})"; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 720f13874..a3fa754af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -167,7 +167,7 @@ namespace Barotrauma public Point StartPos, EndPos; - public bool DisplayOnSonar; + public readonly HashSet MissionsToDisplayOnSonar = new HashSet(); public readonly CaveGenerationParams CaveGenerationParams; @@ -334,13 +334,15 @@ namespace Barotrauma LevelGenParams, Size, GenStart, - TunnelGen, + TunnelGen1, + TunnelGen2, AbyssGen, CaveGen, VoronoiGen, VoronoiGen2, VoronoiGen3, Ruins, + Outposts, FloatingIce, LevelBodies, IceSpires, @@ -512,7 +514,6 @@ namespace Barotrauma GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16); - GenerateEqualityCheckValue(LevelGenStage.TunnelGen); LevelObjectManager = new LevelObjectManager(); @@ -574,7 +575,7 @@ namespace Barotrauma (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); endExitPosition = new Point(endPosition.X, borders.Bottom); - GenerateEqualityCheckValue(LevelGenStage.TunnelGen); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen1); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels @@ -670,15 +671,15 @@ namespace Barotrauma CalculateTunnelDistanceField(null); GenerateSeaFloorPositions(); - GenerateEqualityCheckValue(LevelGenStage.AbyssGen); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen2); GenerateAbyssArea(); - GenerateEqualityCheckValue(LevelGenStage.CaveGen); + GenerateEqualityCheckValue(LevelGenStage.AbyssGen); GenerateCaves(mainPath); - GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); + GenerateEqualityCheckValue(LevelGenStage.CaveGen); //---------------------------------------------------------------------------------- //generate voronoi sites @@ -784,7 +785,7 @@ namespace Barotrauma } } - GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); //---------------------------------------------------------------------------------- // construct the voronoi graph and cells @@ -895,7 +896,7 @@ namespace Barotrauma startPosition.X = (int)pathCells[0].Site.Coord.X; startExitPosition.X = startPosition.X; - GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level @@ -933,8 +934,12 @@ namespace Barotrauma } List ruinPositions = new List(); - int ruinCount = GenerationParams.RuinCount; - if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false) + int ruinCount = GenerationParams.UseRandomRuinCount() + ? Rand.Range(GenerationParams.MinRuinCount, GenerationParams.MaxRuinCount + 1, Rand.RandSync.ServerAndClient) + : GenerationParams.RuinCount; + + bool hasRuinMissions = GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false; + if (hasRuinMissions) { ruinCount = Math.Max(ruinCount, 1); } @@ -1131,7 +1136,7 @@ namespace Barotrauma } } - GenerateEqualityCheckValue(LevelGenStage.Ruins); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3); //---------------------------------------------------------------------------------- // create some ruins @@ -1141,10 +1146,10 @@ namespace Barotrauma for (int i = 0; i < ruinPositions.Count; i++) { Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i); - GenerateRuin(ruinPositions[i], mirror); + GenerateRuin(ruinPositions[i], mirror, hasRuinMissions); } - GenerateEqualityCheckValue(LevelGenStage.FloatingIce); + GenerateEqualityCheckValue(LevelGenStage.Ruins); //---------------------------------------------------------------------------------- // create floating ice chunks @@ -1176,7 +1181,7 @@ namespace Barotrauma } } - GenerateEqualityCheckValue(LevelGenStage.LevelBodies); + GenerateEqualityCheckValue(LevelGenStage.FloatingIce); //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells @@ -1281,7 +1286,7 @@ namespace Barotrauma } #endif - GenerateEqualityCheckValue(LevelGenStage.IceSpires); + GenerateEqualityCheckValue(LevelGenStage.LevelBodies); //---------------------------------------------------------------------------------- // create ice spires @@ -1294,6 +1299,8 @@ namespace Barotrauma if (spire != null) { ExtraWalls.Add(spire); }; } + GenerateEqualityCheckValue(LevelGenStage.IceSpires); + //---------------------------------------------------------------------------------- // connect side paths and cave branches to their parents //---------------------------------------------------------------------------------- @@ -1316,7 +1323,7 @@ namespace Barotrauma CreateOutposts(); - GenerateEqualityCheckValue(LevelGenStage.TopAndBottom); + GenerateEqualityCheckValue(LevelGenStage.Outposts); //---------------------------------------------------------------------------------- // top barrier & sea floor @@ -1325,7 +1332,8 @@ namespace Barotrauma TopBarrier = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)), ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0))); - + //for debugging purposes + TopBarrier.UserData = "topbarrier"; TopBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, borders.Height)), 0.0f); TopBarrier.BodyType = BodyType.Static; TopBarrier.CollisionCategories = Physics.CollisionLevel; @@ -1358,15 +1366,23 @@ namespace Barotrauma CreateWrecks(); CreateBeaconStation(); - GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects); + GenerateEqualityCheckValue(LevelGenStage.TopAndBottom); LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); - GenerateEqualityCheckValue(LevelGenStage.GenerateItems); + GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects); GenerateItems(); - GenerateEqualityCheckValue(LevelGenStage.Finish); + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.Info.IsOutpost) + { + OutpostGenerator.PowerUpOutpost(sub); + } + } + + GenerateEqualityCheckValue(LevelGenStage.GenerateItems); #if CLIENT backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); @@ -1384,7 +1400,7 @@ namespace Barotrauma } //initialize MapEntities that aren't in any sub (e.g. items inside ruins) - MapEntity.MapLoaded(MapEntity.mapEntityList.FindAll(me => me.Submarine == null), false); + MapEntity.MapLoaded(MapEntity.MapEntityList.FindAll(me => me.Submarine == null), false); Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); @@ -1409,6 +1425,7 @@ namespace Barotrauma GameMain.Server.EntityEventManager.Clear(); #endif + GenerateEqualityCheckValue(LevelGenStage.Finish); //assign an ID to make entity events work //ID = FindFreeID(); Generating = false; @@ -1949,7 +1966,8 @@ namespace Barotrauma BottomBarrier = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)), ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0))); - + //for debugging purposes + BottomBarrier.UserData = "bottombarrier"; BottomBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, BottomPos)), 0.0f); BottomBarrier.BodyType = BodyType.Static; BottomBarrier.CollisionCategories = Physics.CollisionLevel; @@ -2055,23 +2073,56 @@ namespace Barotrauma } } - private void GenerateRuin(Point ruinPos, bool mirror) + private void GenerateRuin(Point ruinPos, bool mirror, bool requireMissionReadyRuin) { - var ruinGenerationParams = RuinGenerationParams.RuinParams.GetRandom(Rand.RandSync.ServerAndClient); + float GetWeight(RuinGenerationParams p) + { + if (p.PreferredDifficulty == -1) { return 100; } + float diff = Math.Abs(Difficulty - p.PreferredDifficulty) / 100; + float weight = MathUtils.Pow(1 - diff, 10); + return Math.Max(weight, 0); + } + IEnumerable possibleRuinGenerationParams = RuinGenerationParams.RuinParams; + if (requireMissionReadyRuin) + { + possibleRuinGenerationParams = possibleRuinGenerationParams.Where(p => p.IsMissionReady); + } + if (possibleRuinGenerationParams.Multiple()) + { + // Sort by weight and choose from the closest 25% of the candidates. + // Prevents choosing from the "wrong" end, which would otherwise be possible (yet rare), because we use a weighted random for the pick. + possibleRuinGenerationParams = possibleRuinGenerationParams + /* the prefabs aren't in a consistent order, so we need to sort them first to ensure the clients and server choose the same one */ + .OrderByDescending(p => p.UintIdentifier) + .OrderByDescending(GetWeight) + .Take((int)Math.Max(Math.Round(possibleRuinGenerationParams.Count() / 4f), 1)); + } + var selectedRuinGenerationParams = possibleRuinGenerationParams.GetRandomByWeight(GetWeight, randSync: Rand.RandSync.ServerAndClient); + if (selectedRuinGenerationParams == null) + { + DebugConsole.ThrowError("Failed to generate alien ruins. Could not find any RuinGenerationParameters!"); + return; + } + DebugConsole.NewMessage($"Creating alien ruins using {selectedRuinGenerationParams.Identifier} (preferred difficulty: {selectedRuinGenerationParams.PreferredDifficulty}, current difficulty {Difficulty})", color: Color.Yellow); LocationType locationType = StartLocation?.Type; if (locationType == null) { locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); - if (ruinGenerationParams.AllowedLocationTypes.Any()) + if (selectedRuinGenerationParams.AllowedLocationTypes.Any()) { locationType = LocationType.Prefabs.Where(lt => - ruinGenerationParams.AllowedLocationTypes.Any(allowedType => + selectedRuinGenerationParams.AllowedLocationTypes.Any(allowedType => allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient); } } - var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror); + var ruin = new Ruin(this, selectedRuinGenerationParams, locationType, ruinPos, mirror); + if (ruin.Submarine != null) + { + SetLinkedSubCrushDepth(ruin.Submarine); + } + Ruins.Add(ruin); var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4); @@ -3671,7 +3722,7 @@ namespace Barotrauma } tempSW.Stop(); Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); - sub.SetPosition(spawnPoint); + sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false); wreckPositions.Add(sub, positions); blockedRects.Add(sub, rects); return sub; @@ -3985,11 +4036,13 @@ namespace Barotrauma SubmarineInfo outpostInfo; Submarine outpost = null; - if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) + + SubmarineInfo preSelectedOutpost = isStart ? preSelectedStartOutpost : preSelectedEndOutpost; + if (preSelectedOutpost == null) { if (LevelData.OutpostGenerationParamsExist) { - Location location = i == 0 ? StartLocation : EndLocation; + Location location = isStart ? StartLocation : EndLocation; OutpostGenerationParams outpostGenerationParams = null; if (LevelData.ForceOutpostGenerationParams != null) { @@ -4027,7 +4080,7 @@ namespace Barotrauma foreach (string categoryToHide in locationType.HideEntitySubcategories) { - foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) + foreach (MapEntity entityToHide in MapEntity.MapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) { entityToHide.HiddenInGame = true; } @@ -4048,7 +4101,7 @@ namespace Barotrauma else { DebugConsole.NewMessage($"Loading a pre-selected outpost for the {(isStart ? "start" : "end")} of the level..."); - outpostInfo = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; + outpostInfo = preSelectedOutpost; outpostInfo.Type = SubmarineType.Outpost; outpost = new Submarine(outpostInfo); } @@ -4132,6 +4185,7 @@ namespace Barotrauma } outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); + SetLinkedSubCrushDepth(outpost); foreach (WayPoint wp in WayPoint.WayPointList) { @@ -4178,7 +4232,7 @@ namespace Barotrauma ContentFile contentFile = null; if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); if (contentFile == null) { DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); @@ -4266,77 +4320,75 @@ namespace Barotrauma beaconSonar.Item.CreateServerEvent(beaconSonar); #endif } - else + else if (GameMain.NetworkMember is not { IsClient: true }) { - if (!(GameMain.NetworkMember?.IsClient ?? false)) + bool allowDisconnectedWires = true; + bool allowDamagedWalls = true; + if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) { - bool allowDisconnectedWires = true; - bool allowDamagedWalls = true; - if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) - { - allowDisconnectedWires = info.AllowDisconnectedWires; - allowDamagedWalls = info.AllowDamagedWalls; - } + allowDisconnectedWires = info.AllowDisconnectedWires; + allowDamagedWalls = info.AllowDamagedWalls; + } - //remove wires - float removeWireMinDifficulty = 20.0f; - float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (removeWireProbability > 0.0f && allowDisconnectedWires) + //remove wires + float removeWireMinDifficulty = 20.0f; + float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; + if (removeWireProbability > 0.0f && allowDisconnectedWires) + { + foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { - foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + Wire wire = item.GetComponent(); + if (wire.Locked) { continue; } + if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) { - if (item.NonInteractable || item.InvulnerableToDamage) { continue; } - Wire wire = item.GetComponent(); - if (wire.Locked) { continue; } - if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) + continue; + } + if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) + { + continue; + } + if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < removeWireProbability) + { + foreach (Connection connection in wire.Connections) { - continue; - } - if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) - { - continue; - } - if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < removeWireProbability) - { - foreach (Connection connection in wire.Connections) + if (connection != null) { - if (connection != null) - { - connection.ConnectionPanel.DisconnectedWires.Add(wire); - wire.RemoveConnection(connection.Item); + connection.ConnectionPanel.DisconnectedWires.Add(wire); + wire.RemoveConnection(connection.Item); #if SERVER - connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); - wire.CreateNetworkEvent(); + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); #endif - } } } } } + } - if (allowDamagedWalls) + if (allowDamagedWalls) + { + //break powered items + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) { - //break powered items - foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) { - if (item.NonInteractable || item.InvulnerableToDamage) { continue; } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) - { - item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); - } + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); } - //poke holes in the walls - foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) + } + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) + { + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) { - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) - { - int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); - structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); - } + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); } } } } + SetLinkedSubCrushDepth(BeaconStation); } public bool CheckBeaconActive() @@ -4345,7 +4397,15 @@ namespace Barotrauma return beaconSonar.Voltage > beaconSonar.MinVoltage && beaconSonar.CurrentMode == Sonar.Mode.Active; } - private bool IsModeStartOutpostCompatible() + private void SetLinkedSubCrushDepth(Submarine parentSub) + { + foreach (var connectedSub in parentSub.GetConnectedSubs()) + { + connectedSub.RealWorldCrushDepth = Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000); + } + } + + private static bool IsModeStartOutpostCompatible() { #if CLIENT return GameMain.GameSession?.GameMode is CampaignMode || GameMain.GameSession?.GameMode is TutorialMode || GameMain.GameSession?.GameMode is TestGameMode; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index b7a5aea58..77c3a7cf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -178,7 +178,7 @@ namespace Barotrauma if (eventSet is null) { continue; } int count = childElement.GetAttributeInt("count", 0); if (count < 1) { continue; } - FinishedEvents.Add(eventSet, count); + FinishedEvents.TryAdd(eventSet, count); } static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 1a1210460..66e83c546 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -502,9 +502,19 @@ namespace Barotrauma } } - [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public bool UseRandomRuinCount() => MinRuinCount >= 0 && MaxRuinCount > 0; + + public int GetMaxRuinCount() => UseRandomRuinCount() ? MaxRuinCount : RuinCount; + + [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level. Ignored, if both MinRuinCount and MaxRuinCount are defined."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } + [Serialize(0, IsPropertySaveable.Yes, description: "The minimum number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int MinRuinCount { get; set; } + + [Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int MaxRuinCount { get; set; } + // TODO: Move the wreck parameters under a separate class? #region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 1bec94d04..462235bed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma private List updateableObjects; private List[,] objectGrid; + const float ParallaxStrength = 0.0001f; + public float GlobalForceDecreaseTimer { get; @@ -237,7 +239,6 @@ namespace Barotrauma PlaceObject(prefab, spawnPosition, level, cave); if (amount > prefab.MaxCount && objects.Count > prefab.MaxCount) { - bool maxReached = false; int objectCount = 0; for (int j = 0; j < objects.Count; j++) { @@ -406,11 +407,11 @@ namespace Barotrauma } } - float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z / 10000.0f; - float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z / 10000.0f; + float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z * ParallaxStrength; + float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z * ParallaxStrength; - float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z / 10000.0f - level.BottomPos; - float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z / 10000.0f - level.BottomPos; + float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z * ParallaxStrength - level.BottomPos; + float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z * ParallaxStrength - level.BottomPos; if (newObject.Triggers != null) { @@ -465,7 +466,7 @@ namespace Barotrauma for (int y = yStart; y <= yEnd; y++) { var list = objectGrid[x, y]; - if (objectGrid[x, y] == null) { list = objectGrid[x, y] = new List(); } + if (list == null) { objectGrid[x, y] = list = new List(); } //insertion sort in ascending order (= prefer rendering objects in front) int drawOrderIndex = 0; @@ -478,9 +479,9 @@ namespace Barotrauma } } - public static Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition) + public static Point GetGridIndices(Vector2 worldPosition) { - return new Microsoft.Xna.Framework.Point( + return new Point( (int)Math.Floor(worldPosition.X / GridSize), (int)Math.Floor((worldPosition.Y - Level.Loaded.BottomPos) / GridSize)); } @@ -521,7 +522,7 @@ namespace Barotrauma return objectsInRange; } - private static List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) + private static List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType) { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 60d7ce783..1aadaa354 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -366,6 +366,7 @@ namespace Barotrauma private void LoadElements(LevelObjectPrefabsFile file, ContentXElement element, int parentTriggerIndex) { int propertyOverrideCount = 0; + //load sprites first, OverrideProperties may need them (defaulting to the default sprite if no override is defined) foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -384,6 +385,12 @@ namespace Barotrauma case "deformablesprite": DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); break; + } + } + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { case "overridecommonness": Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 501c9d163..a0b834335 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -27,6 +27,9 @@ namespace Barotrauma.RuinGeneration public override string Name => "RuinGenerationParams"; + [Serialize(true, IsPropertySaveable.Yes, description: "Are these params designed to be used for alien ruins targeted by missions. If false, the params are ignored when there's any missions targeting ruins."), Editable] + public bool IsMissionReady { get; set; } + public RuinGenerationParams(ContentXElement element, RuinConfigFile file) : base(element, file) { } public static void SaveAll() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 72504a631..4d4d7ab3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -44,7 +44,7 @@ namespace Barotrauma.RuinGeneration //prevent the ruin from extending above the level "ceiling" position.Y = Math.Min(level.Size.Y - (Submarine.Borders.Height / 2) - 100, position.Y); - Submarine.SetPosition(position.ToVector2()); + Submarine.SetPosition(position.ToVector2(), forceUndockFromStaticSubmarines: false); if (mirror) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 58ae53c65..1cae8abfe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -398,7 +398,8 @@ namespace Barotrauma } } - if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles) + if (GameMain.GameSession?.GameMode is CampaignMode campaign && + (campaign.PurchasedLostShuttles || campaign.PurchasedLostShuttlesInLatestSave)) { foreach (Structure wall in Structure.WallList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 296df273b..03ebcb980 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -69,9 +69,11 @@ namespace Barotrauma public int LocationTypeChangeCooldown; /// - /// Is some mission blocking this location from changing its type? + /// Is some mission blocking this location from changing its type, or have location type changes been forcibly disabled on the location? /// - public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + + public bool DisallowLocationTypeChanges; public string BaseName { get => baseName; } @@ -1146,6 +1148,7 @@ namespace Barotrauma foreach (Item item in items) { if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; } + if (item.IsSalvageMissionItem) { continue; } if (item.OriginalModuleIndex < 0) { DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost."); @@ -1417,7 +1420,7 @@ namespace Barotrauma public void Reset(CampaignMode campaign) { - if (Type != OriginalType) + if (Type != OriginalType && !DisallowLocationTypeChanges) { ChangeType(campaign, OriginalType); PendingLocationTypeChange = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index f2211a472..475fd10e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -41,6 +41,8 @@ namespace Barotrauma public bool IsEnterable { get; private set; } + public bool AllowAsBiomeGate { get; private set; } + /// /// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone /// @@ -120,6 +122,7 @@ namespace Barotrauma UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true); AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index b3c614074..bbb70a021 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -614,13 +614,20 @@ namespace Barotrauma Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ? Connections[i].Locations[0] : Connections[i].Locations[1]; - if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") + if (!AllowAsBiomeGate(leftMostLocation.Type)) { leftMostLocation.ChangeType( campaign, - LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)), createStores: false); } + static bool AllowAsBiomeGate(LocationType lt) + { + //checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate + //but might be better to keep it for backwards compatibility (previously we relied only on that check) + return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate; + } + leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; @@ -784,6 +791,28 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null)); } + private Location GetPreviousToEndLocation() + { + Location previousToEndLocation = null; + foreach (Location location in Locations) + { + if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X)) + { + previousToEndLocation = location; + } + } + return previousToEndLocation; + } + + private void ForceLocationTypeToNone(CampaignMode campaign, Location location) + { + if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) + { + location.ChangeType(campaign, locationType, createStores: false); + } + location.DisallowLocationTypeChanges = true; + } + private void CreateEndLocation(CampaignMode campaign) { float zoneWidth = Width / generationParams.DifficultyZones; @@ -800,15 +829,7 @@ namespace Barotrauma } } - Location previousToEndLocation = null; - foreach (Location location in Locations) - { - if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X)) - { - previousToEndLocation = location; - } - } - + var previousToEndLocation = GetPreviousToEndLocation(); if (endLocation == null || previousToEndLocation == null) { return; } endLocations = new List() { endLocation }; @@ -833,10 +854,7 @@ namespace Barotrauma } } - if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) - { - previousToEndLocation.ChangeType(campaign, locationType, createStores: false); - } + ForceLocationTypeToNone(campaign, previousToEndLocation); //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) @@ -1555,6 +1573,7 @@ namespace Barotrauma if (index < 0) { return null; } return Locations[index]; } + } void Discover(Location location) @@ -1604,6 +1623,12 @@ namespace Barotrauma SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation)); } } + + var previousToEndLocation = GetPreviousToEndLocation(); + if (previousToEndLocation != null) + { + ForceLocationTypeToNone(campaign, previousToEndLocation); + } } public void Save(XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index 1e1e136c2..a510fd2f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -110,7 +110,13 @@ namespace Barotrauma return; } - radiationAffliction ??= new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount); + if (radiationAffliction == null) + { + float radiationStrengthChange = AfflictionPrefab.RadiationSickness.Effects.FirstOrDefault()?.StrengthChange ?? 0.0f; + radiationAffliction = new Affliction( + AfflictionPrefab.RadiationSickness, + (Params.RadiationDamageAmount - radiationStrengthChange) * Params.RadiationDamageDelay); + } radiationTimer = Params.RadiationDamageDelay; @@ -120,11 +126,9 @@ namespace Barotrauma if (IsEntityRadiated(character)) { - foreach (Limb limb in character.AnimController.Limbs) - { - AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false); - character.CharacterHealth.ApplyDamage(limb, attackResult); - } + var limb = character.AnimController.MainLimb; + AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false); + character.CharacterHealth.ApplyDamage(limb, attackResult); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 09fcedaf3..39f35865a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -12,7 +12,7 @@ namespace Barotrauma { abstract partial class MapEntity : Entity, ISpatialEntity { - public static List mapEntityList = new List(); + public readonly static List MapEntityList = new List(); public readonly MapEntityPrefab Prefab; @@ -82,8 +82,8 @@ namespace Barotrauma { get { return isHighlighted || ExternalHighlight; } set - { - if (value != IsHighlighted) + { + if (value != isHighlighted) { isHighlighted = value; CheckIsHighlighted(); @@ -531,32 +531,32 @@ namespace Barotrauma { if (Sprite == null) { - mapEntityList.Add(this); + MapEntityList.Add(this); return; } int i = 0; - while (i < mapEntityList.Count) + while (i < MapEntityList.Count) { i++; - if (mapEntityList[i - 1]?.Prefab == Prefab) + if (MapEntityList[i - 1]?.Prefab == Prefab) { - mapEntityList.Insert(i, this); + MapEntityList.Insert(i, this); return; } } #if CLIENT i = 0; - while (i < mapEntityList.Count) + while (i < MapEntityList.Count) { i++; - Sprite existingSprite = mapEntityList[i - 1].Sprite; + Sprite existingSprite = MapEntityList[i - 1].Sprite; if (existingSprite == null) { continue; } if (existingSprite.Texture == this.Sprite.Texture) { break; } } #endif - mapEntityList.Insert(i, this); + MapEntityList.Insert(i, this); } /// @@ -566,7 +566,7 @@ namespace Barotrauma { base.Remove(); - mapEntityList.Remove(this); + MapEntityList.Remove(this); if (aiTarget != null) aiTarget.Remove(); } @@ -575,7 +575,7 @@ namespace Barotrauma { base.Remove(); - mapEntityList.Remove(this); + MapEntityList.Remove(this); #if CLIENT Submarine.ForceRemoveFromVisibleEntities(this); @@ -638,6 +638,7 @@ namespace Barotrauma sw.Restart(); #endif Powered.UpdatePower(deltaTime); + Item.UpdatePendingConditionUpdates(deltaTime); foreach (Item item in Item.ItemList) { item.Update(deltaTime, cam); @@ -689,6 +690,20 @@ namespace Barotrauma { IdRemap idRemap = new IdRemap(parentElement, idOffset); + bool containsHiddenContainers = false; + bool hiddenContainerCreated = false; + MTRandom hiddenContainerRNG = new MTRandom(ToolBox.StringToInt(submarine.Info.Name)); + foreach (var element in parentElement.Elements()) + { + if (element.NameAsIdentifier() != "Item") { continue; } + var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); + if (tags.Contains(Tags.HiddenItemContainer)) + { + containsHiddenContainers = true; + break; + } + } + List entities = new List(); foreach (var element in parentElement.Elements()) { @@ -713,10 +728,11 @@ namespace Barotrauma continue; } + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + Identifier replacementIdentifier = Identifier.Empty; if (t == typeof(Structure)) { string name = element.Attribute("name").Value; - Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null) { @@ -727,6 +743,20 @@ namespace Barotrauma } } } + else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && + submarine.Info.Type == SubmarineType.Player && !submarine.Info.HasTag(SubmarineTag.Shuttle)) + { + if (!hiddenContainerCreated) + { + DebugConsole.AddWarning($"There are no hidden containers such as loose vents or loose panels in the submarine \"{submarine.Info.Name}\". Certain traitor events require these to function properly. Converting one of the vents to a loose vent..."); + } + if (!hiddenContainerCreated || hiddenContainerRNG.NextDouble() < 0.2) + { + replacementIdentifier = "loosevent".ToIdentifier(); + containsHiddenContainers = true; + hiddenContainerCreated = true; + } + } try { @@ -741,7 +771,12 @@ namespace Barotrauma } else { - object newEntity = loadMethod.Invoke(t, new object[] { element.FromPackage(null), submarine, idRemap }); + var newElement = element.FromPackage(null); + if (!replacementIdentifier.IsEmpty) + { + newElement.SetAttributeValue("identifier", replacementIdentifier.ToString()); + } + object newEntity = loadMethod.Invoke(t, new object[] { newElement, submarine, idRemap }); if (newEntity != null) { entities.Add((MapEntity)newEntity); @@ -774,9 +809,9 @@ namespace Barotrauma for (int i = 0; i < entities.Count; i++) { if (entities[i].mapLoadedCalled || entities[i].Removed) { continue; } - if (entities[i] is LinkedSubmarine) + if (entities[i] is LinkedSubmarine sub) { - linkedSubs.Add((LinkedSubmarine)entities[i]); + linkedSubs.Add(sub); continue; } @@ -795,6 +830,35 @@ namespace Barotrauma { linkedSub.OnMapLoaded(); } + + CreateDroppedStacks(entities); + } + + private static void CreateDroppedStacks(List entities) + { + const float MaxDist = 10.0f; + List itemsInStack = new List(); + for (int i = 0; i < entities.Count; i++) + { + if (entities[i] is not Item item1 || item1.Prefab.MaxStackSize <= 1 || item1.body is not { Enabled: true }) { continue; } + itemsInStack.Clear(); + itemsInStack.Add(item1); + for (int j = i + 1; j < entities.Count; j++) + { + if (entities[j] is not Item item2) { continue; } + if (item1.Prefab != item2.Prefab) { continue; } + if (item2.body is not { Enabled: true }) { continue; } + if (item2.DroppedStack.Any()) { continue; } + if (Math.Abs(item1.Position.X - item2.Position.X) > MaxDist) { continue; } + if (Math.Abs(item1.Position.Y - item2.Position.Y) > MaxDist) { continue; } + itemsInStack.Add(item2); + } + if (itemsInStack.Count > 1) + { + item1.CreateDroppedStack(itemsInStack, allowClientExecute: true); + DebugConsole.Log($"Merged x{itemsInStack.Count} of {item1.Name} into a dropped stack."); + } + } } public static void InitializeLoadedLinks(IEnumerable entities) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 2c9624004..ab5a6f72b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -158,11 +158,22 @@ namespace Barotrauma { if (name.IsNullOrEmpty()) { throw new ArgumentException($"{nameof(name)} must not be null or empty"); } - return Find(prefab => - prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || - prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || - (prefab.Aliases != null && - prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))); + var matches = + List.Where(prefab => + prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + (prefab.Aliases != null && + prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))); + //if there's multiple matches, prefer ones that aren't hidden in menus and base items + //(hidden ones and variants are often e.g. some kind of special ones used in an event or versions unlockable with talents) + if (matches.Count() > 1) + { + var bestMatch = + matches.FirstOrDefault(prefab => !prefab.HideInMenus) ?? + matches.FirstOrDefault(prefab => prefab is ItemPrefab ip && ip.VariantOf.IsEmpty); + if (bestMatch != null) { return bestMatch; } + } + return matches.FirstOrDefault(); } public static MapEntityPrefab FindByIdentifier(Identifier identifier) @@ -210,6 +221,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool HideInMenus { get; protected set; } + [Serialize(false, IsPropertySaveable.No)] + public bool HideInEditors { get; protected set; } + [Serialize("", IsPropertySaveable.No)] public string Subcategory { get; protected set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 35a758882..b55acd04e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -31,6 +31,12 @@ namespace Barotrauma set; } + [Serialize(-1, IsPropertySaveable.Yes, description: "The closer to the current level difficulty this value is, the higher the probability of choosing these generation params are. Defaults to -1, which means we use the current difficulty."), Editable(MinValueInt = 1, MaxValueInt = 50)] + public int PreferredDifficulty + { + get; + set; + } [Serialize(10, IsPropertySaveable.Yes, description: "Total number of modules in the outpost."), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 5f410a025..5c0da41f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Security.Cryptography; namespace Barotrauma { @@ -130,7 +129,7 @@ namespace Barotrauma //remove linked subs too if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); } } - List entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine)); + List entities = MapEntity.MapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine)); entities.ForEach(e => e.Remove()); foreach (Submarine otherSub in connectedSubs) { @@ -201,7 +200,14 @@ namespace Barotrauma selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); - AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); + + AppendToModule( + selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, + selectedModules, + locationType, + allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams, + allowDifferentLocationType: remainingTries == 1); + if (pendingModuleFlags.Any(flag => flag != "none")) { if (!allowInvalidOutpost) @@ -442,7 +448,6 @@ namespace Barotrauma } } AlignLadders(selectedModules, entities); - PowerUpOutpost(entities.SelectMany(e => e.Value)); if (generationParams.MaxWaterPercentage > 0.0f) { foreach (var entity in allEntities) @@ -533,67 +538,67 @@ namespace Barotrauma /// Attaches additional modules to all the available gaps of the given module, /// and continues recursively through the attached modules until all the pending module types have been placed. /// - /// The module to attach to - /// Which modules we can choose from - /// Which types of modules we still need in the outpost + /// The module to attach to. + /// Which modules we can choose from. + /// Which types of modules we still need in the outpost. /// The modules we've already selected to be used in the outpost. + /// The type of the location we're generating the outpost for. + /// If we fail to append to the current module, should we try replacing it with something else and see if we can append to it then? + /// Is the module allowed to be placed further down than the initial module (usually the airlock module)? + /// If we fail to find a module suitable for the location type, should we use a module that's meant for a different location type instead? private static bool AppendToModule(PlacedModule currentModule, List availableModules, List pendingModuleFlags, List selectedModules, LocationType locationType, - bool retry = true, - bool allowExtendBelowInitialModule = false) + bool tryReplacingCurrentModule = true, + bool allowExtendBelowInitialModule = false, + bool allowDifferentLocationType = false) { if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - for (int i = 0; i < 2; i++) + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { - //try placing a module meant for this location type first, and if that fails, try choosing whatever fits - bool allowDifferentLocationType = i > 0; - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) + if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } + if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } + + PlacedModule newModule = null; + //try appending to the current module if possible + if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) { - if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } - - PlacedModule newModule = null; - //try appending to the current module if possible - if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) - { - newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); - } - - if (newModule != null) - { - placedModules.Add(newModule); - } - else - { - //couldn't append to current module, try one of the other placed modules - foreach (PlacedModule otherModule in selectedModules) - { - if (otherModule == currentModule) { continue; } - foreach (OutpostModuleInfo.GapPosition otherGapPosition in - GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) - { - if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } - newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); - if (newModule != null) - { - placedModules.Add(newModule); - break; - } - } - if (newModule != null) { break; } - } - } - if (pendingModuleFlags.Count == 0) { return true; } + newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); } - } + + if (newModule != null) + { + placedModules.Add(newModule); + } + else + { + //couldn't append to current module, try one of the other placed modules + foreach (PlacedModule otherModule in selectedModules) + { + if (otherModule == currentModule) { continue; } + foreach (OutpostModuleInfo.GapPosition otherGapPosition in + GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) + { + if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } + newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + if (newModule != null) + { + placedModules.Add(newModule); + break; + } + } + if (newModule != null) { break; } + } + } + if (pendingModuleFlags.Count == 0) { return true; } + } //couldn't place a module anywhere, we're probably fucked! - if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) + if (placedModules.Count == 0 && tryReplacingCurrentModule && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) { //try to replace the previously placed module with something else that we can append to for (int i = 0; i < 10; i++) @@ -607,7 +612,7 @@ namespace Barotrauma currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true); assertAllPreviousModulesPresent(); if (currentModule == null) { break; } - if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) + if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, tryReplacingCurrentModule: false, allowExtendBelowInitialModule, allowDifferentLocationType)) { assertAllPreviousModulesPresent(); return true; @@ -618,7 +623,7 @@ namespace Barotrauma foreach (PlacedModule placedModule in placedModules) { - AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule); + AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, tryReplacingCurrentModule: true, allowExtendBelowInitialModule, allowDifferentLocationType); } return placedModules.Count > 0; @@ -929,10 +934,8 @@ namespace Barotrauma if (!suitableModules.Any()) { - if (allowDifferentLocationType) + if (allowDifferentLocationType && modulesWithCorrectFlags.Any()) { - if (modulesWithCorrectFlags.Any()) - DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } @@ -1353,44 +1356,38 @@ namespace Barotrauma if (startWaypoint.WorldPosition.X > endWaypoint.WorldPosition.X) { - var temp = startWaypoint; - startWaypoint = endWaypoint; - endWaypoint = temp; + (endWaypoint, startWaypoint) = (startWaypoint, endWaypoint); } if (hallwayLength > 100 && isHorizontal) { + //if the hallway is longer than 100 pixels, generate some waypoints inside it + //for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders WayPoint prevWayPoint = startWaypoint; + WayPoint firstWayPoint = null; for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f) { var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub); + firstWayPoint ??= newWayPoint; prevWayPoint.linkedTo.Add(newWayPoint); newWayPoint.linkedTo.Add(prevWayPoint); prevWayPoint = newWayPoint; } + if (firstWayPoint != null) + { + firstWayPoint.linkedTo.Add(startWaypoint); + startWaypoint.linkedTo.Add(firstWayPoint); + } if (prevWayPoint != null) { prevWayPoint.linkedTo.Add(endWaypoint); endWaypoint.linkedTo.Add(prevWayPoint); } } - - WayPoint closestWaypoint = null; - float closestDistSqr = 30.0f * 30.0f; - foreach (WayPoint waypoint in WayPoint.WayPointList) + else { - if (waypoint == startWaypoint) { continue; } - float dist = Vector2.DistanceSquared(waypoint.WorldPosition, startWaypoint.WorldPosition); - if (dist < closestDistSqr) - { - closestWaypoint = waypoint; - closestDistSqr = dist; - } - } - if (closestWaypoint != null) - { - startWaypoint.linkedTo.Add(closestWaypoint); - closestWaypoint.linkedTo.Add(startWaypoint); + startWaypoint.linkedTo.Add(endWaypoint); + endWaypoint.linkedTo.Add(startWaypoint); } } } @@ -1444,7 +1441,7 @@ namespace Barotrauma { foreach (MapEntity me in entities[module]) { - if (!(me is Gap gap)) { continue; } + if (me is not Gap gap) { continue; } var door = gap.ConnectedDoor; if (door != null && !door.UseBetweenOutpostModules) { continue; } if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap)) @@ -1524,15 +1521,38 @@ namespace Barotrauma static void RemoveLinkedEntity(MapEntity linked) { - if (linked is Item linkedItem && linkedItem.Connections != null) + if (linked is Item linkedItem) { - foreach (Connection connection in linkedItem.Connections) + if (linkedItem.Connections != null) { - foreach (Wire w in connection.Wires.ToArray()) + foreach (Connection connection in linkedItem.Connections) { - w?.Item.Remove(); + foreach (Wire w in connection.Wires.ToArray()) + { + w?.Item.Remove(); + } } } + //if we end up removing a ladder, remove its waypoints too + if (linkedItem.GetComponent() is Ladder ladder) + { + var ladderWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Ladders == ladder); + foreach (var ladderWaypoint in ladderWaypoints) + { + //got through all waypoints linked to the ladder waypoints, and link them together + //so we don't end up breaking up any paths by removing the ladder waypoints + for (int i = 0; i < ladderWaypoint.linkedTo.Count; i++) + { + if (ladderWaypoint.linkedTo[i] is not WayPoint waypoint1 || waypoint1.Ladders == ladder) { continue; } + for (int j = i + 1; j < ladderWaypoint.linkedTo.Count; j++) + { + if (ladderWaypoint.linkedTo[j] is not WayPoint waypoint2 || waypoint2.Ladders == ladder) { continue; } + waypoint1.ConnectTo(waypoint2); + } + } + } + ladderWaypoints.ForEach(wp => wp.Remove()); + } } linked.Remove(); } @@ -1610,11 +1630,15 @@ namespace Barotrauma } } - private static void PowerUpOutpost(IEnumerable entities) + public static void PowerUpOutpost(Submarine sub) { - foreach (Entity e in entities) + //create a copy of the list, because EntitySpawner may not exist yet we're generating the level, + //which can cause items to be removed/instantiated directly + var entities = MapEntity.MapEntityList.Where(me => me.Submarine == sub).ToList(); + + foreach (MapEntity e in entities) { - if (!(e is Item item)) { continue; } + if (e is not Item item) { continue; } var reactor = item.GetComponent(); if (reactor != null) { @@ -1626,9 +1650,9 @@ namespace Barotrauma for (int i = 0; i < 600; i++) { Powered.UpdatePower((float)Timing.Step); - foreach (Entity e in entities) + foreach (MapEntity e in entities) { - if (!(e is Item item) || item.GetComponent() == null) { continue; } + if (e is not Item item || item.GetComponent() == null) { continue; } item.Update((float)Timing.Step, GameMain.GameScreen.Cam); } } @@ -1683,7 +1707,7 @@ namespace Barotrauma gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.ServerAndClient); } characterInfo.TeamID = CharacterTeamType.FriendlyNPC; - var npc = Character.Create(CharacterPrefab.HumanSpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + var npc = Character.Create(characterInfo.SpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; npc.HumanPrefab = humanPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 535ade368..0569e112d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -81,8 +81,6 @@ namespace Barotrauma get { return base.Prefab.Sprite; } } - public bool IsExteriorWall { get; private set; } = true; - public bool IsPlatform { get { return Prefab.Platform; } @@ -406,8 +404,6 @@ namespace Barotrauma } } - CheckIsExteriorWall(); - #if CLIENT convexHulls?.ForEach(x => x.Move(amount)); @@ -700,41 +696,15 @@ namespace Barotrauma } } - public void CheckIsExteriorWall() + private static Vector2[] CalculateExtremes(Rectangle sectionRect) { - if (!HasBody) - { - IsExteriorWall = false; - return; - } + Vector2[] corners = new Vector2[4]; + corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height); + corners[1] = new Vector2(sectionRect.X, sectionRect.Y); + corners[2] = new Vector2(sectionRect.Right, sectionRect.Y); + corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height); - Vector2 point1 = WorldPosition + BodyOffset * Scale; - //point1 = MathUtils.RotatePointAroundTarget(WorldPosition, point1, BodyRotation); - Vector2 point2 = point1; - - Vector2 normal = new Vector2( - (float)-Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), - (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)); - - float thickness = IsHorizontal ? - (BodyHeight > 0 ? BodyHeight : rect.Height) : - (BodyWidth > 0 ? BodyWidth : rect.Width); - - point1 += normal * (thickness / 2 + 16); - point2 -= normal * (thickness / 2 + 16); - - IsExteriorWall = - Hull.FindHullUnoptimized(point1, null, useWorldCoordinates: true) == null || - Hull.FindHullUnoptimized(point2, null, useWorldCoordinates: true) == null; -#if CLIENT - if (convexHulls != null) - { - foreach (ConvexHull ch in convexHulls) - { - ch.IsExteriorWall = IsExteriorWall; - } - } -#endif + return corners; } /// @@ -742,9 +712,9 @@ namespace Barotrauma /// public static Structure GetAttachTarget(Vector2 worldPosition) { - foreach (MapEntity mapEntity in mapEntityList) + foreach (MapEntity mapEntity in MapEntityList) { - if (mapEntity is not Structure structure) { continue; } + if (!(mapEntity is Structure structure)) { continue; } if (!structure.Prefab.AllowAttachItems) { continue; } if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; } Rectangle worldRect = mapEntity.WorldRect; @@ -1273,7 +1243,7 @@ namespace Barotrauma UpdateSections(); } - private void CreateWallDamageExplosion(Gap gap, Character attacker) + private static void CreateWallDamageExplosion(Gap gap, Character attacker) { const float explosionRange = 750.0f; float explosionStrength = gap.Open; @@ -1294,20 +1264,23 @@ namespace Barotrauma if (explosionOnBroken == null) { - explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); + explosionOnBroken = new Explosion(explosionRange, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations)) { - explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null); + explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(3.0f), null); } else { - explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); + explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(3.0f), null); } + explosionOnBroken.IgnoreCover = true; explosionOnBroken.OnlyInside = true; explosionOnBroken.DisableParticles(); } + explosionOnBroken.Attack.Range = explosionRange * gap.Open; explosionOnBroken.Attack.DamageMultiplier = explosionStrength; + explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f); explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker); #if CLIENT if (linkedHull != null) @@ -1669,7 +1642,6 @@ namespace Barotrauma { SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false); } - CheckIsExteriorWall(); } public virtual void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 57d2d0424..fad619258 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -361,11 +361,11 @@ namespace Barotrauma List pumps = new List(); List allItems = GetItems(true); - bool anyHasTag = allItems.Any(i => i.HasTag("ballast")); + bool anyHasTag = allItems.Any(i => i.HasTag(Tags.Ballast)); foreach (Item item in allItems) { - if ((!anyHasTag || item.HasTag("ballast")) && item.GetComponent() is { } pump) + if ((!anyHasTag || item.HasTag(Tags.Ballast)) && item.GetComponent() is { } pump) { pumps.Add(pump); } @@ -624,11 +624,18 @@ namespace Barotrauma //math/physics stuff ---------------------------------------------------- - public static Vector2 VectorToWorldGrid(Vector2 position) + public static Vector2 VectorToWorldGrid(Vector2 position, bool round = false) { - position.X = (float)Math.Floor(position.X / GridSize.X) * GridSize.X; - position.Y = (float)Math.Ceiling(position.Y / GridSize.Y) * GridSize.Y; - + if (round) + { + position.X = MathF.Round(position.X / GridSize.X) * GridSize.X; + position.Y = MathF.Round(position.Y / GridSize.Y) * GridSize.Y; + } + else + { + position.X = MathF.Floor(position.X / GridSize.X) * GridSize.X; + position.Y = MathF.Ceiling(position.Y / GridSize.Y) * GridSize.Y; + } return position; } @@ -636,7 +643,7 @@ namespace Barotrauma { List entities = onlyHulls ? Hull.HullList.FindAll(h => h.Submarine == this).Cast().ToList() : - MapEntity.mapEntityList.FindAll(me => me.Submarine == this); + MapEntity.MapEntityList.FindAll(me => me.Submarine == this); //ignore items whose body is disabled (wires, items inside cabinets) entities.RemoveAll(e => @@ -693,6 +700,22 @@ namespace Barotrauma return new Rectangle((int)pos.X, (int)pos.Y, (int)size.X, (int)size.Y); } + public static RectangleF AbsRectF(Vector2 pos, Vector2 size) + { + if (size.X < 0.0f) + { + pos.X += size.X; + size.X = -size.X; + } + if (size.Y < 0.0f) + { + pos.Y += size.Y; + size.Y = -size.Y; + } + + return new RectangleF(pos.X, pos.Y, size.X, size.Y); + } + public static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive = false) { if (inclusive) @@ -721,6 +744,18 @@ namespace Barotrauma } } + public static bool RectsOverlap(RectangleF rect1, RectangleF rect2, bool inclusive = true) + { + if (inclusive) + { + return !(rect1.X > rect2.X + rect2.Width || rect1.X + rect1.Width < rect2.X || + rect1.Y < rect2.Y - rect2.Height || rect1.Y - rect1.Height > rect2.Y); + } + + return !(rect1.X >= rect2.X + rect2.Width || rect1.X + rect1.Width <= rect2.X || + rect1.Y <= rect2.Y - rect2.Height || rect1.Y - rect1.Height >= rect2.Y); + } + public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate customPredicate = null, bool allowInsideFixture = false) { if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.0001f) @@ -965,7 +1000,7 @@ namespace Barotrauma Item.UpdateHulls(); List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); - List subEntities = MapEntity.mapEntityList.FindAll(me => me.Submarine == this); + List subEntities = MapEntity.MapEntityList.FindAll(me => me.Submarine == this); foreach (MapEntity e in subEntities) { @@ -1047,7 +1082,7 @@ namespace Barotrauma public void EnableFactionSpecificEntities(Identifier factionIdentifier) { - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; } @@ -1202,7 +1237,7 @@ namespace Barotrauma if (item.Submarine != this) { continue; } var pump = item.GetComponent(); if (pump == null || item.CurrentHull == null) { continue; } - if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } pump.FlowPercentage = 0.0f; ballastHulls.Add(item.CurrentHull); } @@ -1326,7 +1361,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList.ToList()) { if (!connectedSubs.Contains(item.Submarine)) { continue; } - if (!item.HasTag("cargocontainer")) { continue; } + if (!item.HasTag(Tags.CargoContainer)) { continue; } if (item.NonInteractable || item.HiddenInGame) { continue; } var itemContainer = item.GetComponent(); if (itemContainer == null) { continue; } @@ -1358,22 +1393,38 @@ namespace Barotrauma } /// - /// Finds the sub whose borders contain the position + /// Finds the sub whose borders contain the position. Note that this method uses the "actual" position of the sub outside the level: + /// only use this if the position is in a submarine's local coordinate space! /// - public static Submarine FindContaining(Vector2 position) + public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f) { foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; - subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); - - subBorders.Inflate(500.0f, 500.0f); - - if (subBorders.Contains(position)) return sub; + subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height); + subBorders.Inflate(inflate, inflate); + if (subBorders.Contains(position)) { return sub; } } return null; } + + /// + /// Finds the sub whose world borders contain the position. + /// + public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f) + { + foreach (Submarine sub in Loaded) + { + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint() - new Point(0, sub.Borders.Height); + worldBorders.Inflate(inflate, inflate); + if (worldBorders.Contains(worldPosition)) { return sub; } + } + return null; + } + + public static Rectangle GetBorders(XElement submarineElement) { Vector4 bounds = Vector4.Zero; @@ -1430,7 +1481,7 @@ namespace Barotrauma HiddenSubPosition = new Vector2( //1st sub on the left side, 2nd on the right, etc - HiddenSubPosition.X * (i % 2 == 0 ? 1 : -1), + -HiddenSubPosition.X, HiddenSubPosition.Y + sub.Borders.Height + 5000.0f); } @@ -1479,7 +1530,7 @@ namespace Barotrauma center.X -= center.X % GridSize.X; center.Y -= center.Y % GridSize.Y; - RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); + RepositionEntities(-center, MapEntity.MapEntityList.Where(me => me.Submarine == this)); } subBody = new SubmarineBody(this, showErrorMessages); @@ -1497,7 +1548,7 @@ namespace Barotrauma !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (me.Submarine != this) { continue; } if (me is Item item) @@ -1554,16 +1605,16 @@ namespace Barotrauma } entityGrid = Hull.GenerateEntityGrid(this); - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + for (int i = 0; i < MapEntity.MapEntityList.Count; i++) { - if (MapEntity.mapEntityList[i].Submarine != this) { continue; } - MapEntity.mapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); + if (MapEntity.MapEntityList[i].Submarine != this) { continue; } + MapEntity.MapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); } Loading = false; MapEntity.MapLoaded(newEntities, true); - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (me.Submarine != this) { continue; } if (me is LinkedSubmarine linkedSub) @@ -1653,7 +1704,7 @@ namespace Barotrauma public bool CheckFuel() { - float fuel = GetItems(true).Where(i => i.HasTag("reactorfuel")).Sum(i => i.Condition); + float fuel = GetItems(true).Where(i => i.HasTag(Tags.Fuel)).Sum(i => i.Condition); Info.LowFuel = fuel < 200; return !Info.LowFuel; } @@ -1681,7 +1732,7 @@ namespace Barotrauma element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); var cargoContainers = GetCargoContainers(); int cargoCapacity = cargoContainers.Sum(c => c.container.Capacity); - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) { @@ -1702,13 +1753,12 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { - if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap == null) { continue; } - foreach (var (requiredTag, swapTo) in item.PendingItemSwap.SwappableItem.ConnectedItemsToSwap) + if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap is not { } connectedItemsToSwap) { continue; } + foreach (var (requiredTag, swapTo) in connectedItemsToSwap) { List itemsToSwap = new List(); itemsToSwap.AddRange(item.linkedTo.Where(lt => (lt as Item)?.HasTag(requiredTag) ?? false).Cast()); - var connectionPanel = item.GetComponent(); - if (connectionPanel != null) + if (item.GetComponent() is ConnectionPanel connectionPanel) { foreach (Connection c in connectionPanel.Connections) { @@ -1730,13 +1780,13 @@ namespace Barotrauma foreach (Item itemToSwap in itemsToSwap) { itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap; - if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; } + if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; } } } } Dictionary savedEntities = new Dictionary(); - foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) + foreach (MapEntity e in MapEntity.MapEntityList.OrderBy(e => e.ID)) { if (!e.ShouldBeSaved) { continue; } @@ -1980,25 +2030,33 @@ namespace Barotrauma { var connectedWp = connection.Waypoint; if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; } - Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; - Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; - var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); - if (body != null) + bool isObstructed = wp.CurrentHull is Hull h && h.Submarine != this; + if (!isObstructed) { - if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; + Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; + var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); + if (body != null) { - connectedWp.IsObstructed = true; - wp.IsObstructed = true; - if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) + if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { - nodes = new HashSet(); - obstructedNodes.Add(otherSub, nodes); + isObstructed = true; } - nodes.Add(node); - nodes.Add(connection); - break; } } + if (isObstructed) + { + connectedWp.IsObstructed = true; + wp.IsObstructed = true; + if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) + { + nodes = new HashSet(); + obstructedNodes.Add(otherSub, nodes); + } + nodes.Add(node); + nodes.Add(connection); + break; + } } } } @@ -2054,5 +2112,47 @@ namespace Barotrauma } return selectedContainer; } + + public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? targetWorldPos = null) + { + return targetWorldPos.HasValue ? + GetRelativeSimPositionFromWorldPosition(targetWorldPos.Value, from.Submarine, to.Submarine) : + GetRelativeSimPosition(to.SimPosition, from.Submarine, to.Submarine); + } + + public static Vector2 GetRelativeSimPositionFromWorldPosition(Vector2 targetWorldPos, Submarine fromSub, Submarine toSub) + { + Vector2 worldPos = targetWorldPos; + if (toSub != null) + { + worldPos -= toSub.Position; + } + return GetRelativeSimPosition(ConvertUnits.ToSimUnits(worldPos), fromSub, toSub); + } + + public static Vector2 GetRelativeSimPosition(Vector2 targetSimPos, Submarine fromSub, Submarine toSub) + { + Vector2 targetPos = targetSimPos; + if (fromSub == null && toSub != null) + { + // outside and targeting inside + targetPos += toSub.SimPosition; + } + else if (fromSub != null && toSub == null) + { + // inside and targeting outside + targetPos -= fromSub.SimPosition; + } + else if (fromSub != toSub) + { + if (fromSub != null && toSub != null) + { + // both inside, but in different subs + Vector2 diff = fromSub.SimPosition - toSub.SimPosition; + targetPos -= diff; + } + } + return targetPos; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 064687270..e0d060c04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -163,7 +163,7 @@ namespace Barotrauma { farseerBody.BodyType = BodyType.Static; } - foreach (var mapEntity in MapEntity.mapEntityList) + foreach (var mapEntity in MapEntity.MapEntityList) { if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; } @@ -521,20 +521,25 @@ namespace Barotrauma { if (Submarine.LockY) { return Vector2.Zero; } + //calculate the buoyancy for all connected subs + //doing it separately for each connected sub means e.g. a flooded drone barely + //affects the buoyancy of the main sub even if there was as much water in the + //drone as the whole ballast volume of the sub + var connectedSubs = submarine.GetConnectedSubs(); float waterVolume = 0.0f; float volume = 0.0f; + float totalMass = connectedSubs.Sum(s => s.SubBody.Body.Mass); foreach (Hull hull in Hull.HullList) { - if (hull.Submarine != submarine) { continue; } - + if (hull.Submarine == null || !connectedSubs.Contains(hull.Submarine)) { continue; } + if (hull.Submarine.PhysicsBody is not { BodyType: BodyType.Dynamic }) { continue; } waterVolume += hull.WaterVolume; - volume += hull.Volume; + volume += hull.Volume; } - float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume; - + float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume; float buoyancy = NeutralBallastPercentage - waterPercentage; - + float massRatio = Body.Mass / totalMass; if (buoyancy > 0.0f) { buoyancy *= 2.0f; @@ -543,13 +548,11 @@ namespace Barotrauma { buoyancy = Math.Max(buoyancy, -0.5f); } - if (forceUpwardsTimer > 0.0f) { buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); } - - return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f); + return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f) * massRatio; } public void ApplyForce(Vector2 force) @@ -1009,7 +1012,8 @@ namespace Barotrauma //stun for up to 2 second if the impact equal or higher to the maximum impact if (impact >= MaxCollisionImpact) { - c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(3.0f).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true); + float impactDamage = c.AnimController.GetImpactDamage(impact); + c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 12ea2fef8..a8a0551db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -562,7 +562,7 @@ namespace Barotrauma removals.ForEach(wp => wp.Remove()); removals.Clear(); // Stairs - foreach (MapEntity mapEntity in mapEntityList.ToList()) + foreach (MapEntity mapEntity in MapEntityList.ToList()) { if (!(mapEntity is Structure structure)) { continue; } if (structure.StairDirection == Direction.None) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 704d3f9db..e232ae070 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -282,12 +282,12 @@ namespace Barotrauma.Networking return length; } - public static bool CanUseRadio(Character sender) + public static bool CanUseRadio(Character sender, bool ignoreJamming = false) { - return CanUseRadio(sender, out _); + return CanUseRadio(sender, out _, ignoreJamming); } - public static bool CanUseRadio(Character sender, out WifiComponent radio) + public static bool CanUseRadio(Character sender, out WifiComponent radio, bool ignoreJamming = false) { radio = null; if (sender?.Inventory == null || sender.Removed) { return false; } @@ -295,7 +295,7 @@ namespace Barotrauma.Networking foreach (Item item in sender.Inventory.AllItems) { var wifiComponent = item.GetComponent(); - if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; } + if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit(ignoreJamming) || !sender.HasEquippedItem(item)) { continue; } if (radio == null || wifiComponent.Range > radio.Range) { radio = wifiComponent; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index e88558001..b3b47676e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Networking { if (value != null) { - DebugConsole.NewMessage(value.Name, Microsoft.Xna.Framework.Color.Yellow); + DebugConsole.NewMessage(value.Name, Color.Yellow); } } character = value; @@ -90,12 +90,6 @@ namespace Barotrauma.Networking UsingFreeCam = false; #if CLIENT GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally); - - if (character == GameMain.Client.Character && GameMain.Client.SpawnAsTraitor) - { - character.IsTraitor = true; - character.TraitorCurrentObjective = GameMain.Client.TraitorFirstObjective; - } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 12be53f66..a5067d7c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -194,8 +194,7 @@ namespace Barotrauma } } - private readonly Queue spawnQueue; - private readonly Queue removeQueue; + private readonly Queue> spawnOrRemoveQueue; public abstract class SpawnOrRemove : NetEntityEvent.IData { @@ -255,8 +254,7 @@ namespace Barotrauma public EntitySpawner() : base(null, Entity.EntitySpawnerID) { - spawnQueue = new Queue(); - removeQueue = new Queue(); + spawnOrRemoveQueue = new Queue>(); } public override string ToString() @@ -274,7 +272,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) @@ -287,7 +285,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) @@ -300,7 +298,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) { SpawnIfInventoryFull = spawnIfInventoryFull, IgnoreLimbSlots = ignoreLimbSlots, @@ -318,7 +316,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action onSpawn = null) @@ -331,7 +329,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) @@ -344,13 +342,13 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); } public void AddEntityToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (removeQueue.Contains(entity) || entity.Removed || entity == null || entity.IdFreed) { return; } + if (entity == null || IsInRemoveQueue(entity) || entity.Removed || entity.IdFreed) { return; } if (entity is Item item) { AddItemToRemoveQueue(item); return; } if (entity is Character) { @@ -364,15 +362,15 @@ namespace Barotrauma #endif } - removeQueue.Enqueue(entity); + spawnOrRemoveQueue.Enqueue(entity); } public void AddItemToRemoveQueue(Item item) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (removeQueue.Contains(item) || item.Removed) { return; } + if (IsInRemoveQueue(item) || item.Removed) { return; } - removeQueue.Enqueue(item); + spawnOrRemoveQueue.Enqueue(item); var containedItems = item.OwnInventory?.AllItems; if (containedItems == null) { return; } foreach (Item containedItem in containedItems) @@ -389,7 +387,11 @@ namespace Barotrauma /// public bool IsInSpawnQueue(Predicate predicate) { - return spawnQueue.Any(s => predicate(s)); + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } + } + return false; } /// @@ -397,43 +399,51 @@ namespace Barotrauma /// public int CountSpawnQueue(Predicate predicate) { - return spawnQueue.Count(s => predicate(s)); + int count = 0; + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } + } + return count; } public bool IsInRemoveQueue(Entity entity) { - return removeQueue.Contains(entity); + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } + } + return false; } public void Update(bool createNetworkEvents = true) { if (GameMain.NetworkMember is { IsClient: true }) { return; } - while (spawnQueue.Count > 0) + while (spawnOrRemoveQueue.Count > 0) { - var entitySpawnInfo = spawnQueue.Dequeue(); - - var spawnedEntity = entitySpawnInfo.Spawn(); - if (spawnedEntity == null) { continue; } - - if (createNetworkEvents) + var spawnOrRemove = spawnOrRemoveQueue.Dequeue(); + if (spawnOrRemove.TryGet(out Entity entityToRemove)) + { + if (entityToRemove is Item item) + { + item.SendPendingNetworkUpdates(); + } + if (createNetworkEvents) + { + CreateNetworkEventProjSpecific(new RemoveEntity(entityToRemove)); + } + entityToRemove.Remove(); + } + else if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo)) { - CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); + var spawnedEntity = spawnInfo.Spawn(); + if (spawnedEntity == null) { continue; } + if (createNetworkEvents) + { + CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); + } + spawnInfo.OnSpawned(spawnedEntity); } - entitySpawnInfo.OnSpawned(spawnedEntity); - } - - while (removeQueue.Count > 0) - { - var removedEntity = removeQueue.Dequeue(); - if (removedEntity is Item item) - { - item.SendPendingNetworkUpdates(); - } - if (createNetworkEvents) - { - CreateNetworkEventProjSpecific(new RemoveEntity(removedEntity)); - } - removedEntity.Remove(); } } @@ -441,8 +451,7 @@ namespace Barotrauma public void Reset() { - removeQueue.Clear(); - spawnQueue.Clear(); + spawnOrRemoveQueue.Clear(); #if CLIENT receivedEvents.Clear(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 4e56ebe95..4c9adeb13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -35,6 +35,7 @@ namespace Barotrauma.Networking MEDICAL, //medical clinic TRANSFER_MONEY, // wallet transfers REWARD_DISTRIBUTION, // wallet reward distribution + CIRCUITBOX, READY_CHECK, READY_TO_SPAWN } @@ -80,11 +81,12 @@ namespace Barotrauma.Networking STARTGAMEFINALIZE, //finalize round initialization ENDGAME, - TRAITOR_MESSAGE, MISSION, EVENTACTION, + TRAITOR_MESSAGE, CREW, //anything related to managing bots in multiplayer MEDICAL, //medical clinic + CIRCUITBOX, MONEY, READY_CHECK //start, end and update a ready check } @@ -112,14 +114,6 @@ namespace Barotrauma.Networking EntityId: entity.ID); } - enum TraitorMessageType - { - Server, - ServerMessageBox, - Objective, - Console - } - enum VoteType { Unknown, @@ -131,7 +125,8 @@ namespace Barotrauma.Networking PurchaseAndSwitchSub, PurchaseSub, SwitchSub, - TransferMoney + TransferMoney, + Traitor, } public enum ReadyCheckState diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs index ee01e2197..40c0e92b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs @@ -61,7 +61,7 @@ namespace Barotrauma.Networking deliveryMethod switch { DeliveryMethod.Reliable => Steamworks.P2PSend.Reliable, - DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Unreliable, + DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Reliable, _ => Steamworks.P2PSend.Unreliable }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index e19220255..658fee626 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Networking /// /// How much skills drop towards the job's default skill levels when dying /// - const float SkillReductionOnDeath = 0.75f; + public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 50.0f; public enum State { @@ -26,7 +26,6 @@ namespace Barotrauma.Networking private readonly NetworkMember networkMember; private readonly Steering shuttleSteering; private readonly List shuttleDoors; - private const string RespawnContainerTag = "respawncontainer"; private readonly ItemContainer respawnContainer; //items created during respawn @@ -109,7 +108,7 @@ namespace Barotrauma.Networking { if (item.Submarine != RespawnShuttle) { continue; } - if (item.HasTag(RespawnContainerTag)) + if (item.HasTag(Tags.RespawnContainer)) { respawnContainer = item.GetComponent(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 91ebdc203..49c55f316 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -42,6 +42,7 @@ namespace Barotrauma.Networking DoSProtection, Karma, Talent, + Traitors, Error, } @@ -59,6 +60,7 @@ namespace Barotrauma.Networking { MessageType.DoSProtection, Color.OrangeRed }, { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Talent, new Color(125, 125, 255) }, + { MessageType.Traitors, new Color(107, 69, 158) }, { MessageType.Error, Color.Red } }; @@ -76,6 +78,7 @@ namespace Barotrauma.Networking { MessageType.DoSProtection, "DoSProtection" }, { MessageType.Karma, "Karma" }, { MessageType.Talent, "Talent" }, + { MessageType.Traitors, "Traitors" }, { MessageType.Error, "Error" } }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 88c0c9776..b6dfb0840 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -15,11 +15,6 @@ namespace Barotrauma.Networking Manual = 0, Random = 1, Vote = 2 } - public enum YesNoMaybe - { - No = 0, Maybe = 1, Yes = 2 - } - public enum BotSpawnMode { Normal, Fill @@ -437,6 +432,16 @@ namespace Barotrauma.Networking private set; } + [Serialize(50f, IsPropertySaveable.Yes)] + /// + /// How much skills drop towards the job's default skill levels when dying + /// + public float SkillLossPercentageOnDeath + { + get; + private set; + } + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { @@ -485,13 +490,6 @@ namespace Barotrauma.Networking private set; } = true; - [Serialize(true, IsPropertySaveable.Yes)] - public bool AllowRagdollButton - { - get; - set; - } - [Serialize(true, IsPropertySaveable.Yes)] public bool AllowFileTransfers { @@ -722,19 +720,33 @@ namespace Barotrauma.Networking set; } - private YesNoMaybe traitorsEnabled; - [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] - public YesNoMaybe TraitorsEnabled + private float traitorProbability; + [Serialize(0.0f, IsPropertySaveable.Yes)] + public float TraitorProbability { - get { return traitorsEnabled; } + get { return traitorProbability; } set { - if (traitorsEnabled == value) { return; } - traitorsEnabled = value; + if (MathUtils.NearlyEqual(traitorProbability, value)) { return; } + traitorProbability = MathHelper.Clamp(value, 0.0f, 1.0f); ServerDetailsChanged = true; } } + + private int traitorDangerLevel; + [Serialize(TraitorEventPrefab.MinDangerLevel, IsPropertySaveable.Yes)] + public int TraitorDangerLevel + { + get { return traitorDangerLevel; } + set + { + int clampedValue = MathHelper.Clamp(value, TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); + if (traitorDangerLevel == clampedValue) { return; } + traitorDangerLevel = clampedValue; + ServerDetailsChanged = true; + } + } [Serialize(defaultValue: 1, isSaveable: IsPropertySaveable.Yes)] public int TraitorsMinPlayerCount { @@ -742,34 +754,13 @@ namespace Barotrauma.Networking set; } - [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMinStartDelay + [Serialize(defaultValue: 50.0f, isSaveable: IsPropertySaveable.Yes)] + public float MinPercentageOfPlayersForTraitorAccusation { get; set; } - [Serialize(defaultValue: 180.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMaxStartDelay - { - get; - set; - } - - [Serialize(defaultValue: 30.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMinRestartDelay - { - get; - set; - } - - [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMaxRestartDelay - { - get; - set; - } - [Serialize(defaultValue: "", IsPropertySaveable.Yes)] public LanguageIdentifier Language { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 72e237b6b..1b9392702 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -30,6 +30,12 @@ namespace Barotrauma public static T HighestVoted(VoteType voteType, IEnumerable voters) { + return HighestVoted(voteType, voters, out _); + } + + public static T HighestVoted(VoteType voteType, IEnumerable voters, out int voteCount) + { + voteCount = 0; if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; } if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; } @@ -52,7 +58,7 @@ namespace Barotrauma selected = votable.Key; } } - + voteCount = highestVotes; return selected; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index c4873a2cf..7f9b856b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -869,11 +869,21 @@ namespace Barotrauma } } - public void UpdateDrawPosition() + public void UpdateDrawPosition(bool interpolate = true) { - drawPosition = Timing.Interpolate(prevPosition, FarseerBody.Position); - drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset); - drawRotation = Timing.InterpolateRotation(prevRotation, FarseerBody.Rotation) + rotationOffset; + if (interpolate) + { + drawPosition = Timing.Interpolate(prevPosition, FarseerBody.Position); + drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset); + drawRotation = Timing.InterpolateRotation(prevRotation, FarseerBody.Rotation) + rotationOffset; + } + else + { + drawPosition = prevPosition = ConvertUnits.ToDisplayUnits(FarseerBody.Position); + drawRotation = prevRotation = FarseerBody.Rotation; + drawOffset = Vector2.Zero; + drawRotation = 0.0f; + } } public void CorrectPosition(List positionBuffer, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 9b8bc37d7..ceb11f4cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -162,26 +162,25 @@ namespace Barotrauma sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Level", sw.ElapsedTicks); - if (Character.Controlled != null) + if (Character.Controlled is { } controlled) { - if (Character.Controlled.SelectedItem != null && Character.Controlled.CanInteractWith(Character.Controlled.SelectedItem)) + if (controlled.SelectedItem != null && controlled.CanInteractWith(controlled.SelectedItem)) { - Character.Controlled.SelectedItem.UpdateHUD(cam, Character.Controlled, (float)deltaTime); + controlled.SelectedItem.UpdateHUD(cam, controlled, (float)deltaTime); } - if (Character.Controlled.Inventory != null) + if (controlled.Inventory != null) { - foreach (Item item in Character.Controlled.Inventory.AllItems) + foreach (Item item in controlled.Inventory.AllItems) { - if (Character.Controlled.HasEquippedItem(item)) + if (controlled.HasEquippedItem(item)) { - item.UpdateHUD(cam, Character.Controlled, (float)deltaTime); + item.UpdateHUD(cam, controlled, (float)deltaTime); } } } } - - sw.Restart(); + sw.Restart(); Character.UpdateAll((float)deltaTime, cam); #elif SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 226e1c3c1..780e23939 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -30,28 +30,12 @@ namespace Barotrauma GameMain.Server.ServerSettings.SelectedLevelDifficulty = difficulty; lastUpdateID++; } -#endif -#if CLIENT +#elif CLIENT levelDifficultyScrollBar.BarScroll = difficulty / 100.0f; levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll); #endif } - public void ToggleTraitorsEnabled(int dir) - { -#if SERVER - if (GameMain.Server == null) return; - - lastUpdateID++; - - int index = (int)GameMain.Server.ServerSettings.TraitorsEnabled + dir; - if (index < 0) index = 2; - if (index > 2) index = 0; - - SetTraitorsEnabled((YesNoMaybe)index); -#endif - } - public void SetBotCount(int botCount) { #if SERVER @@ -89,13 +73,28 @@ namespace Barotrauma #endif } - public void SetTraitorsEnabled(YesNoMaybe enabled) + public void SetTraitorProbability(float probability) { -#if SERVER - if (GameMain.Server != null) GameMain.Server.ServerSettings.TraitorsEnabled = enabled; -#endif + if (GameMain.NetworkMember != null) + { + GameMain.NetworkMember.ServerSettings.TraitorProbability = probability; + } #if CLIENT - traitorProbabilityText.Text = TextManager.Get(enabled.ToString()); + traitorProbabilitySlider.BarScroll = probability; + traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); +#endif + } + + public void SetTraitorDangerLevel(int dangerLevel) + { + if (GameMain.NetworkMember != null) + { + GameMain.NetworkMember.ServerSettings.TraitorDangerLevel = dangerLevel; + } +#if SERVER + if (GameMain.Server != null) { GameMain.Server.ServerSettings.TraitorDangerLevel = dangerLevel; } +#elif CLIENT + SetTraitorDangerIndicators(dangerLevel); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 64032091c..3e477e0e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -85,7 +85,11 @@ namespace Barotrauma AllowRotating, Attachable, HasBody, - Pickable + Pickable, + OnlyByStatusEffectsAndNetwork, + HasIntegratedButtons, + IsToggleableController, + HasConnectionPanel } public bool IsEditable(ISerializableEntity entity) @@ -115,6 +119,26 @@ namespace Barotrauma { return entity is Item item && item.GetComponent() != null; } + case ConditionType.HasIntegratedButtons: + { + return entity is Door door && door.HasIntegratedButtons; + } + case ConditionType.OnlyByStatusEffectsAndNetwork: +#if SERVER + return true; +#else + return false; +#endif + case ConditionType.IsToggleableController: + { + return entity is Controller controller && controller.IsToggle && controller.Item.GetComponent() != null; + } + case ConditionType.HasConnectionPanel: + { + return + (entity is Item item && item.GetComponent() != null) || + (entity is ItemComponent ic && ic.Item.GetComponent() != null); + } } return false; } @@ -228,7 +252,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value + " (not a valid " + PropertyInfo.PropertyType + ")", e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (not a valid {PropertyInfo.PropertyType})", e); return false; } try @@ -237,7 +261,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } } @@ -320,7 +344,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } @@ -403,14 +427,14 @@ namespace Barotrauma PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value).ToIdentifiers().ToArray()); return true; default: - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); - DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); + DebugConsole.ThrowError($"(Cannot convert a string to a {PropertyType})"); return false; } } else if (PropertyType != value.GetType()) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); DebugConsole.ThrowError("(Non-matching type, should be " + PropertyType + " instead of " + value.GetType() + ")"); return false; } @@ -420,7 +444,7 @@ namespace Barotrauma catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } @@ -428,7 +452,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } } @@ -447,7 +471,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } @@ -468,7 +492,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } return true; @@ -488,7 +512,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } return true; @@ -1051,12 +1075,18 @@ namespace Barotrauma { if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); - if (savedVersion >= upgradeVersion) { continue; } - + if (subElement.GetAttributeBool("campaignsaveonly", false)) + { + if ((GameMain.GameSession?.LastSaveVersion ?? GameMain.Version) >= upgradeVersion) { continue; } + } + else + { + if (savedVersion >= upgradeVersion) { continue; } + } foreach (XAttribute attribute in subElement.Attributes()) { var attributeName = attribute.NameAsIdentifier(); - if (attributeName == "gameversion") { continue; } + if (attributeName == "gameversion" || attributeName == "campaignsaveonly") { continue; } if (attributeName == "refreshrect") { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index ebd748ba2..d6d5d5887 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -17,7 +17,7 @@ namespace Barotrauma { public static class XMLExtensions { - private static ImmutableDictionary> converters + private readonly static ImmutableDictionary> Converters = new Dictionary>() { { typeof(string), (str, defVal) => str }, @@ -349,10 +349,11 @@ namespace Barotrauma } return false; } - - public static int GetAttributeInt(this XElement element, string name, int defaultValue) + + public static int GetAttributeInt(this XElement element, string name, int defaultValue) => GetAttributeInt(element?.GetAttribute(name), defaultValue); + + public static int GetAttributeInt(this XAttribute attribute, int defaultValue) { - var attribute = element?.GetAttribute(name); if (attribute == null) { return defaultValue; } int val = defaultValue; @@ -361,12 +362,12 @@ namespace Barotrauma { if (!Int32.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) { - val = (int)float.Parse(element.GetAttribute(name).Value, CultureInfo.InvariantCulture); + val = (int)float.Parse(attribute.Value, CultureInfo.InvariantCulture); } } catch (Exception e) { - LogAttributeError(attribute, element, e); + LogAttributeError(attribute, attribute.Parent, e); } return val; @@ -391,6 +392,25 @@ namespace Barotrauma return val; } + public static ushort GetAttributeUInt16(this XElement element, string name, ushort defaultValue) + { + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + + ushort val = defaultValue; + + try + { + val = ushort.Parse(attribute.Value); + } + catch (Exception e) + { + LogAttributeError(attribute, element, e); + } + + return val; + } + public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { var attribute = element?.GetAttribute(name); @@ -738,8 +758,8 @@ namespace Barotrauma string[] elems = strValue.Split(','); if (elems.Length != 2) { return defaultValue; } - return ((T1)converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1), - (T2)converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2)); + return ((T1)Converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1), + (T2)Converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2)); } public static Point ParsePoint(string stringPoint, bool errorMessages = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index c88eb0204..8c044c67d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -74,11 +74,11 @@ namespace Barotrauma TutorialSkipWarning = true, CorpseDespawnDelay = 600, CorpsesPerSubDespawnThreshold = 5, - #if OSX +#if OSX UseDualModeSockets = false, - #else +#else UseDualModeSockets = true, - #endif +#endif DisableInGameHints = false, EnableSubmarineAutoSave = true, Graphics = GraphicsSettings.GetDefault(), @@ -114,7 +114,7 @@ namespace Barotrauma } #endif //RemoteMainMenuContentUrl gets set to default it left empty - lets allow leaving it empty to make it possible to disable the remote content - if (element.Attribute("RemoteMainMenuContentUrl")?.Value == string.Empty) + if (element.GetAttribute("RemoteMainMenuContentUrl")?.Value == string.Empty) { retVal.RemoteMainMenuContentUrl = string.Empty; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index d2e3f5467..f954f79c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -19,8 +19,7 @@ namespace Barotrauma { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); - LogicalOperator = element.GetAttributeEnum(nameof(LogicalOperator), - element.GetAttributeEnum("comparison", LogicalOperator)); + LogicalOperator = element.GetAttributeEnum("comparison", LogicalOperator); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 55e75b67d..bb39a43b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -1,9 +1,7 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; using System.Xml.Linq; using System.Linq; using Barotrauma.Extensions; -using Barotrauma.IO; using System; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; #if CLIENT @@ -14,28 +12,6 @@ namespace Barotrauma { public partial class Sprite { - public static IEnumerable LoadedSprites - { - get - { - List retVal = null; - lock (list) - { - retVal = list.Select(wRef => - { - if (wRef.TryGetTarget(out Sprite spr)) - { - return spr; - } - return null; - }).Where(s => s != null).ToList(); - } - return retVal; - } - } - - private readonly static List> list = new List>(); - /// /// Reference to the xml element from where the sprite was created. Can be null if the sprite was not defined in xml! /// @@ -119,7 +95,6 @@ namespace Barotrauma return FilePath + ": " + sourceRect; } - public Identifier Identifier { get; private set; } /// /// Identifier of the Map Entity so that we can link the sprite to its owner. /// @@ -130,17 +105,15 @@ namespace Barotrauma partial void CalculateSourceRect(); - private static void AddToList(Sprite elem) - { - lock (list) - { - list.Add(new WeakReference(elem)); - } - } + static partial void AddToList(Sprite sprite); public Sprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) { - if (element is null) { return; } + if (element is null) + { + DebugConsole.ThrowError($"Sprite: xml element null in {file}. Failed to create the sprite!"); + return; + } this.LazyLoad = lazyLoad; SourceElement = element; if (!ParseTexturePath(path, file)) { return; } @@ -171,8 +144,10 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); +#if CLIENT Identifier = GetIdentifier(SourceElement); AddToList(this); +#endif } internal void LoadParams(SpriteParams spriteParams, bool isFlipped) @@ -235,12 +210,11 @@ namespace Barotrauma return $"{sourceElement}{parentElement?.ToString() ?? ""}".ToIdentifier(); } + static partial void RemoveFromList(Sprite sprite); + public void Remove() { - lock (list) - { - list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == this); - } + RemoveFromList(this); DisposeTexture(); } @@ -308,12 +282,18 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); +#if CLIENT Identifier = GetIdentifier(SourceElement); +#endif } } public bool ParseTexturePath(string path = "", string file = "") { +#if SERVER + // Server doesn't care about texture paths at all + return true; +#endif if (file == "") { file = SourceElement.GetAttributeStringUnrestricted("texture", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 514e207b8..8d14f4ce7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,71 +1,246 @@ -using System; +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - // TODO: This class should be refactored: - // - Use XElement instead of XAttribute in the constructor - // - Simplify, remove unnecessary conversions - // - Improve the flow so that the logic is undestandable. - // - Maybe add some test cases for the operators? - class PropertyConditional + /// + /// Conditionals are used by some in-game mechanics to require one + /// or more conditions to be met for those mechanics to be active. + /// For example, some StatusEffects use Conditionals to only trigger + /// if the affected character is alive. + /// + sealed class PropertyConditional { + // TODO: Make this testable and add tests + + /// + /// Category of properties to check against + /// public enum ConditionType { - Uncertain, - PropertyValue, + /// + /// If there exists an affliction with an identifier that matches the given key, check against the strength of that affliction. + /// Otherwise, check against the value of one of the target's properties. + /// + /// The target object's available properties depend on how that object is defined in the [source code](https://github.com/Regalis11/Barotrauma). + /// + /// This is not applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + /// + PropertyValueOrAffliction, + + /// + /// Check against the target character's skill with the same name as the attribute. + /// + /// This is only applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + SkillRequirement, + + /// + /// Check against the name of the target. + /// Name, + + /// + /// Check against the species identifier of the target. Only works on characters. + /// SpeciesName, + + /// + /// Check against the species group of the target. Only works on characters. + /// SpeciesGroup, + + /// + /// Check against the target's tags. Only works on items. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasTag, + + /// + /// Check against the tags of the target's active status effects. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasStatusTag, + + /// + /// Check against the target's specifier tags. In the vanilla game, these are the head index + /// and gender. See human.xml for more details. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasSpecifierTag, - Affliction, + + /// + /// Check against the target's entity type. + /// + /// The currently supported values are "character", "limb", "item", "structure" and "null". + /// EntityType, - LimbType, - SkillRequirement + + /// + /// Check against the target's limb type. See . + /// + LimbType } - public enum Comparison + public enum LogicalOperatorType { And, Or } - public enum OperatorType + /// + /// There are several ways to compare properties to values. + /// The comparison operator to use can be specified by placing one of the following before the value to compare against. + /// + public enum ComparisonOperatorType { None, + + /// + /// Require that the property being checked equals the given value. + /// + /// This is the default operator used if none is specified. + /// Equals, + + /// + /// Require that the property being checked doesn't equal the given value. + /// NotEquals, + + /// + /// Require that the property being checked is less than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThan, + + /// + /// Require that the property being checked is less than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThanEquals, + + /// + /// Require that the property being checked is greater than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThan, + + /// + /// Require that the property being checked is greater than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThanEquals } public readonly ConditionType Type; - public readonly OperatorType Operator; + public readonly ComparisonOperatorType ComparisonOperator; public readonly Identifier AttributeName; public readonly string AttributeValue; - public readonly string[] SplitAttributeValue; + public readonly ImmutableArray AttributeValueAsTags; public readonly float? FloatValue; - public readonly string TargetItemComponentName; + /// + /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. + /// Only works on items. + /// + public readonly string TargetItemComponent; - // Only used by attacks + /// + /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character. + /// Only applies to a character's attacks. + /// public readonly bool TargetSelf; - // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) + /// + /// If set to true, the conditionals defined by this element check against the entity containing the target. + /// public readonly bool TargetContainer; - // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. + + /// + /// If this and TargetContainer are set to true, the conditionals defined by this element check against the entity containing the target's container. + /// For example, diving suits have a status effect that targets contained oxygen tanks, with a conditional that only passes if the locker containing the suit is powered. + /// public readonly bool TargetGrandParent; + /// + /// If set to true, the conditionals defined by this element check against the items contained by the target. Only works with items. + /// public readonly bool TargetContainedItem; - // Remove this after refactoring - public static bool IsValid(XAttribute attribute) + public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) + { + var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); + var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); + var targetSelf = element.GetAttributeBool(nameof(TargetSelf), false); + var targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false); + var targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false); + + ConditionType? overrideConditionType = null; + if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false)) + { + overrideConditionType = ConditionType.SkillRequirement; + } + + foreach (var attribute in element.Attributes()) + { + if (!IsValid(attribute)) { continue; } + if (predicate != null && !predicate(attribute)) { continue; } + + var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); + if (string.IsNullOrWhiteSpace(attributeValueString)) + { + DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); + continue; + } + + var conditionType = overrideConditionType ?? + (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) + ? type + : ConditionType.PropertyValueOrAffliction); + + yield return new PropertyConditional( + attributeName: attribute.NameAsIdentifier(), + comparisonOperator: comparisonOperator, + attributeValue: attributeValueString, + targetItemComponent: targetItemComponent, + targetSelf: targetSelf, + targetContainer: targetContainer, + targetGrandParent: targetGrandParent, + targetContainedItem: targetContainedItem, + conditionType: conditionType); + } + } + + private static bool IsValid(XAttribute attribute) { switch (attribute.Name.ToString().ToLowerInvariant()) { @@ -82,60 +257,62 @@ namespace Barotrauma } } - // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) - public PropertyConditional(XAttribute attribute) + private PropertyConditional( + Identifier attributeName, + ComparisonOperatorType comparisonOperator, + string attributeValue, + string targetItemComponent, + bool targetSelf, + bool targetContainer, + bool targetGrandParent, + bool targetContainedItem, + ConditionType conditionType) { - AttributeName = attribute.NameAsIdentifier(); - string attributeValueString = attribute.Value; - if (string.IsNullOrWhiteSpace(attributeValueString)) - { - DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); - return; - } - string valueString = attributeValueString; - string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } - Operator = GetOperatorType(splitString[0]); + AttributeName = attributeName; - if (Operator == OperatorType.None) - { - Operator = OperatorType.Equals; - valueString = attributeValueString; - } + TargetItemComponent = targetItemComponent; + TargetSelf = targetSelf; + TargetContainer = targetContainer; + TargetGrandParent = targetGrandParent; + TargetContainedItem = targetContainedItem; - TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); - TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); - TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); - TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); - TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); + Type = conditionType; - if (!Enum.TryParse(AttributeName.Value, true, out Type)) - { - Type = ConditionType.Uncertain; - } - - if (attribute.Parent.GetAttributeBool("skillrequirement", false)) - { - Type = ConditionType.SkillRequirement; - } - - AttributeValue = valueString; - SplitAttributeValue = valueString.Split(','); + ComparisonOperator = comparisonOperator; + AttributeValue = attributeValue; + AttributeValueAsTags = AttributeValue.Split(',') + .Select(s => s.ToIdentifier()) + .ToImmutableArray(); if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { FloatValue = value; } } - public static OperatorType GetOperatorType(string op) + public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) + { + str ??= ""; + + ComparisonOperatorType op = ComparisonOperatorType.Equals; + string conditionStr = str; + if (str.IndexOf(' ') is var i and >= 0) + { + op = GetComparisonOperatorType(str[..i]); + if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; } + else { op = ComparisonOperatorType.Equals; } + } + return (op, conditionStr); + } + + public static ComparisonOperatorType GetComparisonOperatorType(string op) { //thanks xml for not letting me use < or > in attributes :( - switch (op) + switch (op.ToLowerInvariant()) { case "e": case "eq": case "equals": - return OperatorType.Equals; + return ComparisonOperatorType.Equals; case "ne": case "neq": case "notequals": @@ -143,311 +320,277 @@ namespace Barotrauma case "!e": case "!eq": case "!equals": - return OperatorType.NotEquals; + return ComparisonOperatorType.NotEquals; case "gt": case "greaterthan": - return OperatorType.GreaterThan; + return ComparisonOperatorType.GreaterThan; case "lt": case "lessthan": - return OperatorType.LessThan; + return ComparisonOperatorType.LessThan; case "gte": case "gteq": case "greaterthanequals": - return OperatorType.GreaterThanEquals; + return ComparisonOperatorType.GreaterThanEquals; case "lte": case "lteq": case "lessthanequals": - return OperatorType.LessThanEquals; + return ComparisonOperatorType.LessThanEquals; default: - return OperatorType.None; + return ComparisonOperatorType.None; } } + private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals; - public bool Matches(ISerializableEntity target) + public bool Matches(ISerializableEntity? target) { - return Matches(target, TargetContainedItem); + return TargetContainedItem + ? MatchesContained(target) + : MatchesDirect(target); } - public bool Matches(ISerializableEntity target, bool checkContained) + private bool MatchesContained(ISerializableEntity? target) { - var type = Type; - if (type == ConditionType.Uncertain) + var containedItems = target switch { - type = AfflictionPrefab.Prefabs.ContainsKey(AttributeName) - ? ConditionType.Affliction - : ConditionType.PropertyValue; - } - - if (checkContained) + Item item + => item.ContainedItems, + ItemComponent ic + => ic.Item.ContainedItems, + Character {Inventory: { } characterInventory} + => characterInventory.AllItems, + _ + => Enumerable.Empty() + }; + foreach (var containedItem in containedItems) { - if (target is Item item) - { - foreach (var containedItem in item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Items.Components.ItemComponent ic) - { - foreach (var containedItem in ic.Item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Character character) - { - if (character.Inventory == null) { return false; } - foreach (var containedItem in character.Inventory.AllItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } + if (MatchesDirect(containedItem)) { return true; } } + return false; + } - switch (type) + private bool MatchesDirect(ISerializableEntity? target) + { + Character? targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + switch (Type) { - case ConditionType.PropertyValue: - SerializableProperty property; - if (target?.SerializableProperties == null) { return Operator == OperatorType.NotEquals; } - if (target.SerializableProperties.TryGetValue(AttributeName, out property)) + case ConditionType.PropertyValueOrAffliction: + // If an AfflictionPrefab with identifier AttributeName exists, + // check for an affliction affecting the target + if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName)) { - return Matches(target, property); + if (targetChar is { CharacterHealth: { } health }) + { + var affliction = health.GetAffliction(AttributeName); + float afflictionStrength = affliction?.Strength ?? 0f; + + return NumberMatchesRequirement(afflictionStrength); + } } - return false; - case ConditionType.Name: - if (target == null) { return Operator == OperatorType.NotEquals; } - return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); + // Otherwise try checking for a property belonging to the target + else if (target?.SerializableProperties != null + && target.SerializableProperties.TryGetValue(AttributeName, out var property)) + { + return PropertyMatchesRequirement(target, property); + } + return ComparisonOperatorIsNotEquals; + case ConditionType.SkillRequirement: + if (targetChar != null) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return NumberMatchesRequirement(skillLevel); + } + return ComparisonOperatorIsNotEquals; case ConditionType.HasTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - return MatchesTagCondition(target); + return ItemMatchesTagCondition(target); case ConditionType.HasStatusTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - int matches = 0; - foreach (DurationListElement durationEffect in StatusEffect.DurationList) + if (target == null) { return ComparisonOperatorIsNotEquals; } + + // NOTE: This can be optimized further by returning + // when a match passes with the Equals operator and + // when a match fails with the NotEquals operator. + // The current form has better readability. + int numMatchingEffects = 0; + int numEffectsAffectingTarget = 0; + + foreach (var durationEffect in StatusEffect.DurationList) { if (!durationEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (durationEffect.Parent.HasTag(tag)) - { - matches++; - } - } + numEffectsAffectingTarget++; + if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { numMatchingEffects++; } } - foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) + + foreach (var delayedEffect in DelayedEffect.DelayList) { if (!delayedEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (delayedEffect.Parent.HasTag(tag)) - { - matches++; - } - } + numEffectsAffectingTarget++; + if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { numMatchingEffects++; } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; - case ConditionType.HasSpecifierTag: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character { Info: { } characterInfo })) { return false; } - return (Operator == OperatorType.Equals) == - SplitAttributeValue.All(v => characterInfo.Head.Preset.TagSet.Contains(v)); - } - case ConditionType.SpeciesName: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); - } - case ConditionType.SpeciesGroup: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Group); - } - case ConditionType.EntityType: - switch (AttributeValue) - { - case "character": - case "Character": - return (Operator == OperatorType.Equals) == target is Character; - case "limb": - case "Limb": - return (Operator == OperatorType.Equals) == target is Limb; - case "item": - case "Item": - return (Operator == OperatorType.Equals) == target is Item; - case "structure": - case "Structure": - return (Operator == OperatorType.Equals) == target is Structure; - case "null": - return (Operator == OperatorType.Equals) == (target == null); - default: - return false; - } - case ConditionType.LimbType: - { - if (!(target is Limb limb)) - { - return false; - } - else - { - return limb.type.ToString().Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); - } - } - case ConditionType.Affliction: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - Character targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - if (targetChar != null) - { - var health = targetChar.CharacterHealth; - if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName.ToIdentifier()); - float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - - return ValueMatchesRequirement(afflictionStrength); - } - } - return false; - case ConditionType.SkillRequirement: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - if (target is Character targetChar) - { - float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); - - return ValueMatchesRequirement(skillLevel); - } - } - return false; + return ComparisonOperatorIsNotEquals + ? numMatchingEffects >= numEffectsAffectingTarget // true when none of the effects have any of the tags + : numMatchingEffects > 0; // true when any effects have all tags default: - return false; + bool equals = CheckOnlyEquality(target); + return ComparisonOperatorIsNotEquals + ? !equals + : equals; } } - private bool ValueMatchesRequirement(float testedValue) + private bool CheckOnlyEquality(ISerializableEntity? target) { - if (FloatValue.HasValue) + switch (Type) { - float value = FloatValue.Value; - switch (Operator) + case ConditionType.Name: + if (target == null) { return false; } + + return target.Name == AttributeValue; + case ConditionType.HasSpecifierTag: { - case OperatorType.Equals: - return testedValue == value; - case OperatorType.GreaterThan: - return testedValue > value; - case OperatorType.GreaterThanEquals: - return testedValue >= value; - case OperatorType.LessThan: - return testedValue < value; - case OperatorType.LessThanEquals: - return testedValue <= value; - case OperatorType.NotEquals: - return testedValue != value; + if (target is not Character {Info: { } characterInfo}) + { + return false; + } + + return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains); + } + case ConditionType.SpeciesName: + { + if (target is Character targetCharacter) + { + return targetCharacter.SpeciesName == AttributeValue; + } + else if (target is Limb targetLimb) + { + return targetLimb.character.SpeciesName == AttributeValue; + } + return false; + } + case ConditionType.SpeciesGroup: + { + if (target is Character targetCharacter) + { + return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group); + } + else if (target is Limb targetLimb) + { + return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetLimb.character.Params.Group); + } + return false; + } + case ConditionType.EntityType: + return AttributeValue.ToLowerInvariant() switch + { + "character" + => target is Character, + "limb" + => target is Limb, + "item" + => target is Item, + "structure" + => target is Structure, + "null" + => target == null, + _ + => false + }; + case ConditionType.LimbType: + { + return target is Limb limb + && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType) + && attributeLimbType == limb.type; } } return false; } - private bool MatchesTagCondition(ISerializableEntity target) + private bool SufficientTagMatches(int matches) { - if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } - - int matches = 0; - foreach (string tag in SplitAttributeValue) - { - if (item.HasTag(tag)) - { - matches++; - } - } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return ComparisonOperatorIsNotEquals + ? matches <= 0 + : matches >= AttributeValueAsTags.Length; } - public bool MatchesTagCondition(Identifier targetTag) + private bool ItemMatchesTagCondition(ISerializableEntity? target) + { + if (target is not Item item) { return ComparisonOperatorIsNotEquals; } + + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (item.HasTag(tag)) { matches++; } + } + return SufficientTagMatches(matches); + } + + public bool TargetTagMatchesTagCondition(Identifier targetTag) { if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; - foreach (string tag in SplitAttributeValue) + foreach (var tag in AttributeValueAsTags) { - if (targetTag == tag) - { - matches++; - } + if (targetTag == tag) { matches++; } } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return SufficientTagMatches(matches); } - // TODO: refactor and add tests - private bool Matches(ISerializableEntity target, SerializableProperty property) + private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) + { + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (statusEffect.HasTag(tag)) { matches++; } + } + return SufficientTagMatches(matches); + } + + private bool NumberMatchesRequirement(float testedValue) + { + if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } + float value = FloatValue.Value; + return CompareFloat(testedValue, value, ComparisonOperator); + } + + private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property) { Type type = property.PropertyType; if (type == typeof(float) || type == typeof(int)) { float floatValue = property.GetFloatValue(target); - switch (Operator) - { - case OperatorType.Equals: - return MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.NotEquals: - return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.GreaterThan: - return floatValue > FloatValue.Value; - case OperatorType.LessThan: - return floatValue < FloatValue.Value; - case OperatorType.GreaterThanEquals: - return floatValue >= FloatValue.Value; - case OperatorType.LessThanEquals: - return floatValue <= FloatValue.Value; - } - return false; + return NumberMatchesRequirement(floatValue); } - switch (Operator) + switch (ComparisonOperator) { - case OperatorType.Equals: + case ComparisonOperatorType.Equals: + case ComparisonOperatorType.NotEquals: + bool equals; + if (type == typeof(bool)) { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); - } - var value = property.GetValue(target); - return Equals(value, AttributeValue); + bool attributeValueBool = AttributeValue.IsTrueString(); + equals = property.GetBoolValue(target) == attributeValueBool; } - case OperatorType.NotEquals: + else { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); - } var value = property.GetValue(target); - return !Equals(value, AttributeValue); + equals = AreValuesEquivalent(value, AttributeValue); } - case OperatorType.GreaterThan: - case OperatorType.LessThanEquals: - case OperatorType.LessThan: - case OperatorType.GreaterThanEquals: + + return ComparisonOperatorIsNotEquals + ? !equals + : equals; + default: DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + "Make sure the type of the value set in the config files matches the type of the property."); - break; + return false; } - return false; - static bool Equals(object value, string desiredValue) + static bool AreValuesEquivalent(object? value, string desiredValue) { if (value == null) { @@ -455,10 +598,31 @@ namespace Barotrauma } else { - return value.ToString().Equals(desiredValue); + return (value.ToString() ?? "").Equals(desiredValue); } } } - } + public static bool CompareFloat(float val1, float val2, ComparisonOperatorType op) + { + switch (op) + { + case ComparisonOperatorType.Equals: + return MathUtils.NearlyEqual(val1, val2); + case ComparisonOperatorType.GreaterThan: + return val1 > val2; + case ComparisonOperatorType.GreaterThanEquals: + return val1 >= val2; + case ComparisonOperatorType.LessThan: + return val1 < val2; + case ComparisonOperatorType.LessThanEquals: + return val1 <= val2; + case ComparisonOperatorType.NotEquals: + return !MathUtils.NearlyEqual(val1, val2); + default: + return false; + } + } + + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 20831e9be..fdf9eaab5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -188,6 +188,10 @@ namespace Barotrauma /// public readonly bool SpawnIfInventoryFull; /// + /// Should the item spawn even if this item isn't in an inventory? Only valid if the SpawnPosition is set to . Defaults to false. + /// + public readonly bool SpawnIfNotInInventory; + /// /// Should the item spawn even if the container can't contain items of this type or if it's already full? /// public readonly bool SpawnIfCantBeContained; @@ -221,6 +225,8 @@ namespace Barotrauma /// public readonly float Condition; + public bool InheritEventTags { get; private set; } + public ItemSpawnInfo(XElement element, string parentDebugName) { if (element.Attribute("name") != null) @@ -250,8 +256,9 @@ namespace Barotrauma } } - SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); - SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); + SpawnIfInventoryFull = element.GetAttributeBool(nameof(SpawnIfInventoryFull), false); + SpawnIfNotInInventory = element.GetAttributeBool(nameof(SpawnIfNotInInventory), false); + SpawnIfCantBeContained = element.GetAttributeBool(nameof(SpawnIfCantBeContained), true); Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("launchimpulse", element.GetAttributeFloat("speed", 0.0f))); Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); @@ -264,6 +271,8 @@ namespace Barotrauma SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); + + InheritEventTags = element.GetAttributeBool(nameof(InheritEventTags), false); } } @@ -398,6 +407,9 @@ namespace Barotrauma "Can be used to for example spawn a character a bit up from the center of an item executing the effect.")] public Vector2 Offset { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool InheritEventTags { get; private set; } + public CharacterSpawnInfo(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -491,11 +503,11 @@ namespace Barotrauma /// public int TargetSlot = -1; - private readonly List requiredItems = new List(); + private readonly List requiredItems; public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects; - private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; + private readonly PropertyConditional.LogicalOperatorType conditionalLogicalOperator = PropertyConditional.LogicalOperatorType.Or; private readonly List propertyConditionals; public bool HasConditions => propertyConditionals != null && propertyConditionals.Any(); @@ -514,15 +526,7 @@ namespace Barotrauma /// /// Can be used in conditionals to check if a StatusEffect with a specific tag is currently running. Only relevant for effects with a non-zero duration. /// - private readonly HashSet tags; - - /// - /// How long the effect runs (in seconds). Note that if is true, - /// there can be multiple instances of the effect running at a time. - /// In other words, if the effect has a duration and executes every frame, you probably want - /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. - /// - private readonly float duration; + private readonly HashSet tags; /// /// How long _can_ the event run (in seconds). The difference to is that @@ -533,7 +537,7 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; - public Dictionary intervalTimers = new Dictionary(); + private Dictionary intervalTimers; /// /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). @@ -576,39 +580,53 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - public readonly List Explosions = new List(); + private readonly List explosions; + public IEnumerable Explosions + { + get { return explosions ?? Enumerable.Empty(); } + } - private readonly List spawnItems = new List(); + private readonly List spawnItems; /// /// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them. /// private readonly bool spawnItemRandomly; - private readonly List spawnCharacters = new List(); + private readonly List spawnCharacters; - public readonly List giveTalentInfos = new List(); + public readonly List giveTalentInfos; - private readonly List aiTriggers = new List(); + private readonly List aiTriggers; - private readonly List triggeredEvents = new List(); + /// + /// A list of events triggered by this status effect. + /// The fields , , + /// can be used to mark the target of the effect, the entity executing it, or the user as targets for the scripted event. + /// + private readonly List triggeredEvents; /// /// If the effect triggers a scripted event, the target of this effect is added as a target for the event using the specified tag. /// For example, an item could have an effect that executes when used on some character, and triggers an event that makes said character say something. /// - private readonly Identifier triggeredEventTargetTag; + private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(); /// /// If the effect triggers a scripted event, the entity executing this effect is added as a target for the event using the specified tag. /// For example, a character could have an effect that executes when the character takes damage, and triggers an event that makes said character say something. /// - private readonly Identifier triggeredEventEntityTag; + private readonly Identifier triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); /// /// If the effect triggers a scripted event, the user of the StatusEffect (= the character who caused it to happen, e.g. a character who used an item) is added as a target for the event using the specified tag. /// For example, a gun could have an effect that executes when a character uses it, and triggers an event that makes said character say something. /// - private readonly Identifier triggeredEventUserTag; + private readonly Identifier triggeredEventUserTag = "statuseffectuser".ToIdentifier(); + + /// + /// Can be used to tag the target entity/entities as targets in an event. + /// + private readonly List<(Identifier eventIdentifier, Identifier tag)> eventTargetTags; private Character user; @@ -651,6 +669,11 @@ namespace Barotrauma /// public readonly ImmutableHashSet TargetIdentifiers; + /// + /// If set to the name of one of the target's ItemComponents, the effect is only applied on that component. + /// Only works on items. + /// + public readonly string TargetItemComponent; /// /// Which type of afflictions the target must receive for the StatusEffect to be applied. Only valid when the type of the effect is OnDamaged. /// @@ -673,16 +696,22 @@ namespace Barotrauma public IEnumerable SpawnCharacters { - get { return spawnCharacters; } + get { return spawnCharacters ?? Enumerable.Empty(); } } public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>(); - private readonly List talentTriggers = new List(); - private readonly List giveExperiences = new List(); - private readonly List giveSkills = new List(); + private readonly List talentTriggers; + private readonly List giveExperiences; + private readonly List giveSkills; - public float Duration => duration; + /// + /// How long the effect runs (in seconds). Note that if is true, + /// there can be multiple instances of the effect running at a time. + /// In other words, if the effect has a duration and executes every frame, you probably want + /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. + /// + public readonly float Duration; /// /// How close to the entity executing the effect the targets must be. Only applicable if targeting NearbyCharacters or NearbyItems. @@ -710,8 +739,8 @@ namespace Barotrauma string[] newTags = value.Split(','); foreach (string tag in newTags) { - string newTag = tag.Trim(); - if (!tags.Contains(newTag)) tags.Add(newTag); + Identifier newTag = tag.Trim().ToIdentifier(); + if (!tags.Contains(newTag)) { tags.Add(newTag); }; } } } @@ -730,20 +759,21 @@ namespace Barotrauma protected StatusEffect(ContentXElement element, string parentDebugName) { - tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); + tags = new HashSet(element.GetAttributeString("tags", "").Split(',').ToIdentifiers()); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false)); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); Interval = element.GetAttributeFloat("interval", 0.0f); - duration = element.GetAttributeFloat("duration", 0.0f); + Duration = element.GetAttributeFloat("duration", 0.0f); disableDeltaTime = element.GetAttributeBool("disabledeltatime", false); setValue = element.GetAttributeBool("setvalue", false); Stackable = element.GetAttributeBool("stackable", true); lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f); CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false); + TargetItemComponent = element.GetAttributeString("targetitemcomponent", string.Empty); TargetSlot = element.GetAttributeInt("targetslot", -1); Range = element.GetAttributeFloat("range", 0.0f); @@ -782,9 +812,9 @@ namespace Barotrauma TargetIdentifiers = targetIdentifiers.ToImmutableHashSet(); } - triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", Identifier.Empty); - triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", Identifier.Empty); - triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", Identifier.Empty); + triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", triggeredEventTargetTag); + triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", triggeredEventEntityTag); + triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", triggeredEventUserTag); spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); @@ -834,7 +864,7 @@ namespace Barotrauma break; case "conditionalcomparison": case "comparison": - if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) + if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator)) { DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } @@ -849,7 +879,7 @@ namespace Barotrauma } break; case "tags": - if (duration <= 0.0f || setValue) + if (Duration <= 0.0f || setValue) { //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: //if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags @@ -866,6 +896,13 @@ namespace Barotrauma } } + if (Duration > 0.0f && !setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. + propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); + } + List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>(); foreach (XAttribute attribute in propertyAttributes) { @@ -878,7 +915,8 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "explosion": - Explosions.Add(new Explosion(subElement, parentDebugName)); + explosions ??= new List(); + explosions.Add(new Explosion(subElement, parentDebugName)); break; case "fire": FireSize = subElement.GetAttributeFloat("size", 10.0f); @@ -909,6 +947,7 @@ namespace Barotrauma break; case "requireditem": case "requireditems": + requiredItems ??= new List(); RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName); if (newRequiredItem == null) { @@ -928,13 +967,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - propertyConditionals.Add(new PropertyConditional(attribute)); - } - } + propertyConditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "affliction": AfflictionPrefab afflictionPrefab; @@ -989,9 +1022,14 @@ namespace Barotrauma break; case "spawnitem": var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName); - if (newSpawnItem.ItemPrefab != null) { spawnItems.Add(newSpawnItem); } + if (newSpawnItem.ItemPrefab != null) + { + spawnItems ??= new List(); + spawnItems.Add(newSpawnItem); + } break; case "triggerevent": + triggeredEvents ??= new List(); Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (!identifier.IsEmpty) { @@ -1009,22 +1047,40 @@ namespace Barotrauma break; case "spawncharacter": var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); - if (!newSpawnCharacter.SpeciesName.IsEmpty) { spawnCharacters.Add(newSpawnCharacter); } + if (!newSpawnCharacter.SpeciesName.IsEmpty) + { + spawnCharacters ??= new List(); + spawnCharacters.Add(newSpawnCharacter); + } break; case "givetalentinfo": var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName); - if (newGiveTalentInfo.TalentIdentifiers.Any()) { giveTalentInfos.Add(newGiveTalentInfo); } + if (newGiveTalentInfo.TalentIdentifiers.Any()) + { + giveTalentInfos ??= new List(); + giveTalentInfos.Add(newGiveTalentInfo); + } break; case "aitrigger": + aiTriggers ??= new List(); aiTriggers.Add(new AITrigger(subElement)); break; case "talenttrigger": + talentTriggers ??= new List(); talentTriggers.Add(subElement.GetAttributeIdentifier("effectidentifier", Identifier.Empty)); break; + case "eventtarget": + eventTargetTags ??= new List<(Identifier eventIdentifier, Identifier tag)>(); + eventTargetTags.Add( + (subElement.GetAttributeIdentifier("eventidentifier", Identifier.Empty), + subElement.GetAttributeIdentifier("tag", Identifier.Empty))); + break; case "giveexperience": + giveExperiences ??= new List(); giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); break; case "giveskill": + giveSkills ??= new List(); giveSkills.Add(new GiveSkill(subElement, parentDebugName)); break; } @@ -1071,7 +1127,7 @@ namespace Barotrauma } else { - return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.TargetTagMatchesTagCondition(t))); } } @@ -1088,7 +1144,7 @@ namespace Barotrauma public virtual bool HasRequiredItems(Entity entity) { - if (entity == null) { return true; } + if (entity == null || requiredItems == null) { return true; } foreach (RelatedItem requiredItem in requiredItems) { if (entity is Item item) @@ -1163,101 +1219,83 @@ namespace Barotrauma return HasRequiredConditions(targets, propertyConditionals); } + private delegate bool ShouldShortCircuit(bool condition, out bool valueToReturn); + + /// + /// Indicates that the Or operator should short-circuit when a condition is true + /// + private static bool ShouldShortCircuitLogicalOrOperator(bool condition, out bool valueToReturn) + { + valueToReturn = true; + return condition; + } + + /// + /// Indicates that the And operator should short-circuit when a condition is false + /// + private static bool ShouldShortCircuitLogicalAndOperator(bool condition, out bool valueToReturn) + { + valueToReturn = false; + return !condition; + } + private bool HasRequiredConditions(IReadOnlyList targets, IReadOnlyList conditionals, bool targetingContainer = false) { if (conditionals.Count == 0) { return true; } - if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } - switch (conditionalComparison) + if (targets.Count == 0 && requiredItems != null && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } + + (ShouldShortCircuit, bool) shortCircuitMethodPair = conditionalLogicalOperator switch { - case PropertyConditional.Comparison.Or: - for (int i = 0; i < conditionals.Count; i++) + PropertyConditional.LogicalOperatorType.Or => (ShouldShortCircuitLogicalOrOperator, false), + PropertyConditional.LogicalOperatorType.And => (ShouldShortCircuitLogicalAndOperator, true), + _ => throw new NotImplementedException() + }; + var (shouldShortCircuit, didNotShortCircuit) = shortCircuitMethodPair; + + for (int i = 0; i < conditionals.Count; i++) + { + bool valueToReturn; + + var pc = conditionals[i]; + if (!pc.TargetContainer || targetingContainer) + { + if (shouldShortCircuit(AnyTargetMatches(targets, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; } + continue; + } + + var target = FindTargetItemOrComponent(targets); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + bool comparisonIsNeq = pc.ComparisonOperator == PropertyConditional.ComparisonOperatorType.NotEquals; + if (shouldShortCircuit(comparisonIsNeq, out valueToReturn)) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - return true; - } - continue; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (pc.Matches(container)) { return true; } - } - else - { - if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } - } - } - if (owner is Character character && pc.Matches(character)) { return true; } - } - else - { - if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } - } + return valueToReturn; } - return false; - case PropertyConditional.Comparison.And: - for (int i = 0; i < conditionals.Count; i++) + continue; + } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - continue; - } - return false; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (!pc.Matches(container)) { return false; } - } - else - { - if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } - } - } - if (owner is Character character && !pc.Matches(character)) { return false; } - } - else - { - if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } - } + //if we're checking for tags, just check the Item object, not the ItemComponents + if (shouldShortCircuit(pc.Matches(container), out valueToReturn)) { return valueToReturn; } } - return true; - default: - throw new NotImplementedException(); + else + { + if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; } + } + } + if (owner is Character character && shouldShortCircuit(pc.Matches(character), out valueToReturn)) { return valueToReturn; } } + return didNotShortCircuit; static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) { @@ -1313,6 +1351,7 @@ namespace Barotrauma { if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; } if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; } + if (!TargetItemComponent.IsNullOrEmpty() && itemComponent.Name != TargetItemComponent) { return false; } if (TargetIdentifiers == null) { return true; } if (TargetIdentifiers.Contains("itemcomponent")) { return true; } if (itemComponent.Item.HasTag(TargetIdentifiers)) { return true; } @@ -1348,9 +1387,10 @@ namespace Barotrauma } private static readonly List intervalsToRemove = new List(); + public bool ShouldWaitForInterval(Entity entity, float deltaTime) { - if (Interval > 0.0f && entity != null) + if (Interval > 0.0f && entity != null && intervalTimers != null) { if (intervalTimers.ContainsKey(entity)) { @@ -1374,13 +1414,13 @@ namespace Barotrauma if (!IsValidTarget(target)) { return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); if (existingEffect != null) { - existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1419,13 +1459,13 @@ namespace Barotrauma return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); if (existingEffect != null) { - existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1528,7 +1568,7 @@ namespace Barotrauma for (int j = 0; j < useItemCount; j++) { if (item.Removed) { continue; } - item.Use(deltaTime, useTargetCharacter, useTargetLimb); + item.Use(deltaTime, user: null, useTargetLimb, useTargetCharacter); } } } @@ -1619,9 +1659,9 @@ namespace Barotrauma } } - if (duration > 0.0f) + if (Duration > 0.0f) { - DurationList.Add(new DurationListElement(this, entity, targets, duration, user)); + DurationList.Add(new DurationListElement(this, entity, targets, Duration, user)); } else { @@ -1649,9 +1689,12 @@ namespace Barotrauma } } - foreach (Explosion explosion in Explosions) + if (explosions != null) { - explosion.Explode(position, damageSource: entity, attacker: user); + foreach (Explosion explosion in explosions) + { + explosion.Explode(position, damageSource: entity, attacker: user); + } } bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; @@ -1660,7 +1703,7 @@ namespace Barotrauma { var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method - if (duration > 0) { break; } + if (Duration > 0) { break; } if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { @@ -1686,6 +1729,7 @@ namespace Barotrauma { if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } + if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); @@ -1735,7 +1779,7 @@ namespace Barotrauma } } - if (aiTriggers.Count > 0) + if (aiTriggers != null) { Character targetCharacter = target as Character; if (targetCharacter == null) @@ -1770,7 +1814,7 @@ namespace Barotrauma } } - if (talentTriggers.Count > 0) + if (talentTriggers != null) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) @@ -1785,16 +1829,19 @@ namespace Barotrauma if (isNotClient) { // these effects do not need to be run clientside, as they are replicated from server to clients anyway - foreach (int giveExperience in giveExperiences) + if (giveExperiences != null) { - Character targetCharacter = CharacterFromTarget(target); - if (targetCharacter != null && !targetCharacter.Removed) + foreach (int giveExperience in giveExperiences) { - targetCharacter?.Info?.GiveExperience(giveExperience); + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + targetCharacter?.Info?.GiveExperience(giveExperience); + } } } - if (giveSkills.Count > 0) + if (giveSkills != null) { foreach (GiveSkill giveSkill in giveSkills) { @@ -1811,7 +1858,7 @@ namespace Barotrauma } } - if (giveTalentInfos.Count > 0) + if (giveTalentInfos != null) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } @@ -1836,6 +1883,17 @@ namespace Barotrauma } } } + + if (eventTargetTags != null) + { + foreach ((Identifier eventId, Identifier tag) in eventTargetTags) + { + if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev) + { + targets.Where(t => t is Entity).ForEach(t => ev.AddTarget(tag, (Entity)t)); + } + } + } } } @@ -1845,7 +1903,7 @@ namespace Barotrauma fire.Size = new Vector2(FireSize, fire.Size.Y); } - if (isNotClient && GameMain.GameSession?.EventManager is { } eventManager) + if (isNotClient && triggeredEvents != null && GameMain.GameSession?.EventManager is { } eventManager) { foreach (EventPrefab eventPrefab in triggeredEvents) { @@ -1877,81 +1935,83 @@ namespace Barotrauma if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { - foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) + if (spawnCharacters != null) { - var characters = new List(); - for (int i = 0; i < characterSpawnInfo.Count; i++) + foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) { - Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, - onSpawn: newCharacter => - { - if (characterSpawnInfo.TotalMaxCount > 0) + var characters = new List(); + for (int i = 0; i < characterSpawnInfo.Count; i++) + { + Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, + onSpawn: newCharacter => { - if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) + if (characterSpawnInfo.TotalMaxCount > 0) { - Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); - return; - } - } - if (newCharacter.AIController is EnemyAIController enemyAi && - enemyAi.PetBehavior != null && - entity is Item item && - item.ParentInventory is CharacterInventory inv) - { - enemyAi.PetBehavior.Owner = inv.Owner as Character; - } - characters.Add(newCharacter); - if (characters.Count == characterSpawnInfo.Count) - { - SwarmBehavior.CreateSwarm(characters.Cast()); - } - if (!characterSpawnInfo.AfflictionOnSpawn.IsEmpty) - { - if (!AfflictionPrefab.Prefabs.TryGet(characterSpawnInfo.AfflictionOnSpawn, out AfflictionPrefab afflictionPrefab)) - { - DebugConsole.NewMessage($"Could not apply an affliction to the spawned character(s). No affliction with the identifier \"{characterSpawnInfo.AfflictionOnSpawn}\" found.", Color.Red); - return; - } - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(characterSpawnInfo.AfflictionStrength)); - } - if (characterSpawnInfo.Stun > 0) - { - newCharacter.SetStun(characterSpawnInfo.Stun); - } - foreach (var target in targets) - { - if (!(target is Character character)) { continue; } - if (characterSpawnInfo.TransferInventory && character.Inventory != null && newCharacter.Inventory != null) - { - if (character.Inventory.Capacity != newCharacter.Inventory.Capacity) { return; } - for (int i = 0; i < character.Inventory.Capacity && i < newCharacter.Inventory.Capacity; i++) + if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) { - character.Inventory.GetItemsAt(i).ForEachMod(item => newCharacter.Inventory.TryPutItem(item, i, allowSwapping: true, allowCombine: false, user: null)); + Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); + return; } } - if (characterSpawnInfo.TransferBuffs || characterSpawnInfo.TransferAfflictions) + if (newCharacter.AIController is EnemyAIController enemyAi && + enemyAi.PetBehavior != null && + entity is Item item && + item.ParentInventory is CharacterInventory inv) { - foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + enemyAi.PetBehavior.Owner = inv.Owner as Character; + } + characters.Add(newCharacter); + if (characters.Count == characterSpawnInfo.Count) + { + SwarmBehavior.CreateSwarm(characters.Cast()); + } + if (!characterSpawnInfo.AfflictionOnSpawn.IsEmpty) + { + if (!AfflictionPrefab.Prefabs.TryGet(characterSpawnInfo.AfflictionOnSpawn, out AfflictionPrefab afflictionPrefab)) { - if (!characterSpawnInfo.TransferAfflictions && characterSpawnInfo.TransferBuffs && affliction.Prefab.IsBuff) + DebugConsole.NewMessage($"Could not apply an affliction to the spawned character(s). No affliction with the identifier \"{characterSpawnInfo.AfflictionOnSpawn}\" found.", Color.Red); + return; + } + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(characterSpawnInfo.AfflictionStrength)); + } + if (characterSpawnInfo.Stun > 0) + { + newCharacter.SetStun(characterSpawnInfo.Stun); + } + foreach (var target in targets) + { + if (target is not Character character) { continue; } + if (characterSpawnInfo.TransferInventory && character.Inventory != null && newCharacter.Inventory != null) + { + if (character.Inventory.Capacity != newCharacter.Inventory.Capacity) { return; } + for (int i = 0; i < character.Inventory.Capacity && i < newCharacter.Inventory.Capacity; i++) { - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); - } - if (characterSpawnInfo.TransferAfflictions) - { - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + character.Inventory.GetItemsAt(i).ForEachMod(item => newCharacter.Inventory.TryPutItem(item, i, allowSwapping: true, allowCombine: false, user: null)); } } - } - if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. - { - if (characterSpawnInfo.TransferControl) + if (characterSpawnInfo.TransferBuffs || characterSpawnInfo.TransferAfflictions) { + foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + { + if (!characterSpawnInfo.TransferAfflictions && characterSpawnInfo.TransferBuffs && affliction.Prefab.IsBuff) + { + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + } + if (characterSpawnInfo.TransferAfflictions) + { + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + } + } + } + if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. + { + if (characterSpawnInfo.TransferControl) + { #if CLIENT if (Character.Controlled == target) - { - Character.Controlled = newCharacter; - } + { + Character.Controlled = newCharacter; + } #elif SERVER foreach (Client c in GameMain.Server.ConnectedClients) { @@ -1960,31 +2020,44 @@ namespace Barotrauma } #endif } - if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } - } - } - }); - } - } - - if (spawnItemRandomly) - { - if (spawnItems.Count > 0) - { - SpawnItem(spawnItems.GetRandomUnsynced()); - } - } - else - { - foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) - { - for (int i = 0; i < itemSpawnInfo.Count; i++) - { - SpawnItem(itemSpawnInfo); + if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } + } + } + if (characterSpawnInfo.InheritEventTags) + { + foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + { + if (activeEvent is ScriptedEvent scriptedEvent) + { + scriptedEvent.InheritTags(entity, newCharacter); + } + } + } + }); } } } + if (spawnItems != null) + { + if (spawnItemRandomly) + { + if (spawnItems.Count > 0) + { + SpawnItem(spawnItems.GetRandomUnsynced()); + } + } + else + { + foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) + { + for (int i = 0; i < itemSpawnInfo.Count; i++) + { + SpawnItem(itemSpawnInfo); + } + } + } + } void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) { @@ -1997,7 +2070,7 @@ namespace Barotrauma switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => + Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { Projectile projectile = newItem.GetComponent(); if (entity != null) @@ -2082,17 +2155,11 @@ namespace Barotrauma rotation += spread; if (projectile != null) { - Vector2 spawnPos; - if (projectile.Hitscan) - { - spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; - } - else - { - spawnPos = ConvertUnits.ToSimUnits(worldPos); - } + var sourceEntity = (sourceBody as ISpatialEntity) ?? entity; + Vector2 spawnPos = sourceEntity.SimPosition; projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + projectile.Item.Submarine = sourceEntity?.Submarine; } else if (newItem.body != null) { @@ -2101,7 +2168,7 @@ namespace Barotrauma newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); } } - newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(newItem, chosenItemSpawnInfo); }); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: @@ -2140,7 +2207,7 @@ namespace Barotrauma allowedSlots.Remove(InvSlotType.Any); character.Inventory.TryPutItem(item, null, allowedSlots); } - item.Condition = item.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(item, chosenItemSpawnInfo); }); } } @@ -2160,7 +2227,14 @@ namespace Barotrauma { Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => { - newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + } + else if (chosenItemSpawnInfo.SpawnIfNotInInventory) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); }); } } @@ -2190,7 +2264,7 @@ namespace Barotrauma { Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => { - newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(newItem, chosenItemSpawnInfo); }); } break; @@ -2199,6 +2273,20 @@ namespace Barotrauma } break; } + void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) + { + newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; + if (itemSpawnInfo.InheritEventTags) + { + foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + { + if (activeEvent is ScriptedEvent scriptedEvent) + { + scriptedEvent.InheritTags(entity, newItem); + } + } + } + } } } @@ -2210,6 +2298,7 @@ namespace Barotrauma } if (Interval > 0.0f && entity != null) { + intervalTimers ??= new Dictionary(); intervalTimers[entity] = Interval; } @@ -2384,7 +2473,7 @@ namespace Barotrauma float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; if (entity is Item sourceItem) { - if (sourceItem.HasTag("medical")) + if (sourceItem.HasTag(Barotrauma.Tags.MedicalItem)) { afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); if (user is not null) @@ -2458,17 +2547,16 @@ namespace Barotrauma DurationList.Clear(); } - public void AddTag(string tag) + public void AddTag(Identifier tag) { if (tags.Contains(tag)) { return; } tags.Add(tag); } - public bool HasTag(string tag) + public bool HasTag(Identifier tag) { if (tag == null) { return true; } - - return (tags.Contains(tag) || tags.Contains(tag.ToLowerInvariant())); + return tags.Contains(tag) || tags.Contains(tag); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs index 94676dc7c..a68b959dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs @@ -1,18 +1,67 @@ -namespace Barotrauma.Steam +#nullable enable +using System; +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma.Steam { static partial class SteamManager { - private static Steamworks.AuthTicket currentTicket = null; - public static Steamworks.AuthTicket GetAuthSessionTicket() + private static Option currentMultiplayerTicket = Option.None; + public static Option GetAuthSessionTicketForMultiplayer(Endpoint remoteHostEndpoint) { if (!IsInitialized) { - return null; + return Option.None; } - currentTicket?.Cancel(); - currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); - return currentTicket; + if (currentMultiplayerTicket.TryUnwrap(out var ticketToCancel)) + { + ticketToCancel.Cancel(); + } + currentMultiplayerTicket = Option.None; + + var netIdentity = remoteHostEndpoint switch + { + LidgrenEndpoint { Address: LidgrenAddress { NetAddress: var ipAddr }, Port: var ipPort } + => (Steamworks.Data.NetIdentity)Steamworks.Data.NetAddress.From(ipAddr, (ushort)ipPort), + SteamP2PEndpoint { SteamId: var steamId } + => (Steamworks.Data.NetIdentity)(Steamworks.SteamId)steamId.Value, + _ + => throw new ArgumentOutOfRangeException(nameof(remoteHostEndpoint)) + }; + var newTicket = Steamworks.SteamUser.GetAuthSessionTicket(netIdentity); + + currentMultiplayerTicket = newTicket != null + ? Option.Some(newTicket) + : Option.None; + + return currentMultiplayerTicket; + } + + private const string GameAnalyticsConsentIdentity = "BarotraumaGameAnalyticsConsent"; + + private static Option currentGameAnalyticsConsentTicket = Option.None; + public static async Task> GetAuthTicketForGameAnalyticsConsent() + { + if (!IsInitialized) + { + return Option.None; + } + + if (currentGameAnalyticsConsentTicket.TryUnwrap(out var ticketToCancel)) + { + ticketToCancel.Cancel(); + } + currentGameAnalyticsConsentTicket = Option.None; + + var newTicket = await Steamworks.SteamUser.GetAuthTicketForWebApi(identity: GameAnalyticsConsentIdentity); + + currentGameAnalyticsConsentTicket = newTicket != null + ? Option.Some(newTicket) + : Option.None; + + return currentGameAnalyticsConsentTicket; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 69e268be3..a832a5abe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -613,7 +613,8 @@ namespace Barotrauma.Steam public static async Task CopyDirectory(string fileListDir, string modName, string from, string to, ShouldCorrectPaths shouldCorrectPaths) { - from = Path.GetFullPath(from); to = Path.GetFullPath(to); + from = Path.GetFullPath(from); + to = Path.GetFullPath(to); Directory.CreateDirectory(to); string convertFromTo(string from) @@ -623,10 +624,16 @@ namespace Barotrauma.Steam string[] subDirs = Directory.GetDirectories(from); foreach (var file in files) { + //ignore hidden files + if (Path.GetFileName(file).StartsWith('.')) { continue; } await CopyFile(fileListDir, modName, file, convertFromTo(file), shouldCorrectPaths); } - foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir), shouldCorrectPaths); } + foreach (var dir in subDirs) + { + if (Path.GetFileName(dir) is { } dirName && dirName.StartsWith('.')) { continue; } + await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir), shouldCorrectPaths); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 6fae4f6cb..c42374a47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -319,7 +319,7 @@ namespace Barotrauma if (causeOfDeath.DamageSource is Item item) { - if (item.HasTag("tool")) + if (item.HasTag(Tags.ToolItem)) { UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs new file mode 100644 index 000000000..282c7d503 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -0,0 +1,102 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +public static class Tags +{ + public static readonly Identifier Fuel = "reactorfuel".ToIdentifier(); + public static readonly Identifier Reactor = "reactor".ToIdentifier(); + + public static readonly Identifier JunctionBox = "junctionbox".ToIdentifier(); + + public static readonly Identifier Turret = "turret".ToIdentifier(); + public static readonly Identifier Hardpoint = "hardpoint".ToIdentifier(); + public static readonly Identifier Periscope = "periscope".ToIdentifier(); + public static readonly Identifier TurretAmmoSource = "turretammosource".ToIdentifier(); + + public static readonly Identifier GeneticMaterial = "geneticmaterial".ToIdentifier(); + public static readonly Identifier GeneticDevice = "geneticdevice".ToIdentifier(); + + public static readonly Identifier HeavyDivingGear = "deepdiving".ToIdentifier(); + public static readonly Identifier LightDivingGear = "lightdiving".ToIdentifier(); + + public static readonly Identifier FPGACircuit = "fpgacircuit".ToIdentifier(); + public static readonly Identifier RedWire = new Identifier("redwire"); + + /// + /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) + /// + public static readonly Identifier DivingGearWearableIndoors = "divinggear_wearableindoors".ToIdentifier(); + + public static readonly Identifier DockingPort = "dock".ToIdentifier(); + public static readonly Identifier Ballast = "ballast".ToIdentifier(); + public static readonly Identifier Airlock = "airlock".ToIdentifier(); + + public static readonly Identifier HiddenItemContainer = "hidden".ToIdentifier(); + + public static readonly Identifier MedicalItem = new Identifier("medical"); + + public static readonly Identifier WeldingFuel = "weldingfuel".ToIdentifier(); + public static readonly Identifier DivingGear = "diving".ToIdentifier(); + public static readonly Identifier OxygenSource = "oxygensource".ToIdentifier(); + public static readonly Identifier FireExtinguisher = "fireextinguisher".ToIdentifier(); + public static readonly Identifier FallbackLocker = "locker".ToIdentifier(); + public static readonly Identifier DontTakeItems = "donttakeitems".ToIdentifier(); + public static readonly Identifier ToolItem = "tool".ToIdentifier(); + public static readonly Identifier LogicItem = "logic".ToIdentifier(); + public static readonly Identifier NavTerminal = "navterminal".ToIdentifier(); + public static readonly Identifier IdCard = "identitycard".ToIdentifier(); + public static readonly Identifier WireItem = "wire".ToIdentifier(); + public static readonly Identifier ChairItem = "chair".ToIdentifier(); + public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); + public static readonly Identifier Thalamus = "thalamus".ToIdentifier(); + + public static readonly Identifier Crate = "crate".ToIdentifier(); + public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); + public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); + + public static readonly Identifier ItemIgnoredByAI = "ignorebyai".ToIdentifier(); + + public static readonly Identifier GuardianShelter = "guardianshelter".ToIdentifier(); + + public static readonly Identifier AllowCleanup = "allowcleanup".ToIdentifier(); + + public static readonly Identifier Weapon = "weapon".ToIdentifier(); + public static readonly Identifier StunnerItem = "stunner".ToIdentifier(); + public static readonly Identifier MobileRadio = "mobileradio".ToIdentifier(); + + /// + /// Any handcuffs. + /// + public static readonly Identifier HandLockerItem = "handlocker".ToIdentifier(); + + /// + /// Vanilla handcuffs. + /// + public static readonly Identifier Handcuffs = "handcuffs".ToIdentifier(); + + /// + /// A battery cell or similar. + /// + public static readonly Identifier MobileBattery = "mobilebattery".ToIdentifier(); + + public static readonly Identifier Traitor = "traitor".ToIdentifier(); + public static readonly Identifier SecondaryTraitor = "secondarytraitor".ToIdentifier(); + public static readonly Identifier AnyTraitor = "anytraitor".ToIdentifier(); + public static readonly Identifier NonTraitor = "nontraitor".ToIdentifier(); + public static readonly Identifier NonTraitorPlayer = "nontraitorplayer".ToIdentifier(); + public static readonly Identifier TraitorMissionItem = "traitormissionitem".ToIdentifier(); + public static readonly Identifier TraitorGuidelinesForSecurity = "traitorguidelinesforsecurity".ToIdentifier(); + + /// + /// Container where the initial gear (diving suit, oxygen tank, etc) of respawning players is placed + /// + public static readonly Identifier RespawnContainer = "respawncontainer".ToIdentifier(); + + /// + /// Container spawned for the gear of a player who despawns (duffel bag) + /// + public static readonly Identifier DespawnContainer = "despawncontainer".ToIdentifier(); + +} + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 3dfef48c5..8fb92ae72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -406,7 +406,7 @@ namespace Barotrauma return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements); } - public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement) + public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement, Func? customTagReplacer = null) { /* @@ -415,21 +415,28 @@ namespace Barotrauma */ - - LocalizedString extraDescriptionLine = Get(descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty)); + Identifier tag = descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty); + LocalizedString extraDescriptionLine = Get(tag).Fallback(tag.Value); foreach (XElement replaceElement in descriptionElement.Elements()) { if (replaceElement.NameAsIdentifier() != "replace") { continue; } - Identifier tag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty); - string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty()); + Identifier variableTag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty); LocalizedString replacementValue = string.Empty; - for (int i = 0; i < replacementValues.Length; i++) + if (customTagReplacer != null) { - replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]); - if (i < replacementValues.Length - 1) + replacementValue = customTagReplacer(replaceElement.GetAttributeString("value", string.Empty)); + } + if (replacementValue.IsNullOrWhiteSpace()) + { + string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty()); + for (int i = 0; i < replacementValues.Length; i++) { - replacementValue += ", "; + replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]); + if (i < replacementValues.Length - 1) + { + replacementValue += ", "; + } } } if (replaceElement.Attribute("color") != null) @@ -437,15 +444,18 @@ namespace Barotrauma string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖"; } - extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); + extraDescriptionLine = extraDescriptionLine.Replace(variableTag, replacementValue); } if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed description += extraDescriptionLine; } - public static LocalizedString FormatCurrency(int amount) + public static LocalizedString FormatCurrency(int amount, bool includeCurrencySymbol = true) { - return GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); + string valueString = string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount); + return includeCurrencySymbol ? + GetWithVariable("currencyformat", "[credits]", valueString) : + valueString; } public static LocalizedString GetServerMessage(string serverMessage) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs new file mode 100644 index 000000000..556ad87e5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs @@ -0,0 +1,126 @@ +#nullable enable +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class TraitorEvent : ScriptedEvent + { + public enum State + { + Incomplete, + Completed, + Failed + } + + public Action? OnStateChanged; + + private new readonly TraitorEventPrefab prefab; + + public new TraitorEventPrefab Prefab => prefab; + + private LocalizedString codeWord; + + private State currentState; + public State CurrentState + { + get { return currentState; } + set + { + if (currentState == value) { return; } + currentState = value; + OnStateChanged?.Invoke(); + } + } + + private Client? traitor; + + public Client? Traitor => traitor; + + private readonly HashSet secondaryTraitors = new HashSet(); + + public IEnumerable SecondaryTraitors => secondaryTraitors; + + public override string ToString() + { + return $"{nameof(TraitorEvent)} ({prefab.Identifier})"; + } + + private readonly static HashSet nonActionChildElementNames = new HashSet() + { + "icon".ToIdentifier(), + "reputationrequirement".ToIdentifier(), + "missionrequirement".ToIdentifier(), + "levelrequirement".ToIdentifier() + }; + protected override IEnumerable NonActionChildElementNames => nonActionChildElementNames; + + public TraitorEvent(TraitorEventPrefab prefab) : base(prefab) + { + this.prefab = prefab; + codeWord = string.Empty; + } + + public override void Init(EventSet? parentSet = null) + { + base.Init(parentSet); + if (traitor == null) + { + DebugConsole.ThrowError($"Error when initializing event \"{prefab.Identifier}\": traitor not set.\n" + Environment.StackTrace); + } + } + + public override LocalizedString ReplaceVariablesInEventText(LocalizedString str) + { + if (codeWord.IsNullOrEmpty()) + { + //store the code word so the same random word is used in all the actions in the event + codeWord = TextManager.Get("traitor.codeword"); + } + + return str + .Replace("[traitor]", traitor?.Name ?? "none") + .Replace("[target]", (GetTargets("target".ToIdentifier()).FirstOrDefault() as Character)?.DisplayName ?? "none") + .Replace("[codeword]", codeWord.Value); + } + + public void SetTraitor(Client traitor) + { + if (traitor.Character == null) + { + throw new InvalidOperationException($"Tried to set a client who's not controlling a character (\"{traitor.Name}\") as the traitor."); + } + this.traitor = traitor; + 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); + } + + public void SetSecondaryTraitors(IEnumerable traitors) + { + int index = 0; + foreach (var traitor in traitors) + { + if (traitor.Character == null) + { + throw new InvalidOperationException($"Tried to set a client who's not controlling a character (\"{traitor.Name}\") as a secondary traitor."); + } + if (this.traitor == traitor) + { + DebugConsole.ThrowError($"Tried to assign the main traitor {traitor.Name} as a secondary traitor."); + continue; + } + secondaryTraitors.Add(traitor); + traitor.Character.IsTraitor = true; + AddTarget(Tags.SecondaryTraitor, traitor.Character); + AddTarget((Tags.SecondaryTraitor.ToString() + index).ToIdentifier(), traitor.Character); + AddTarget(Tags.AnyTraitor, traitor.Character); + index++; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs new file mode 100644 index 000000000..f4e90ae36 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs @@ -0,0 +1,350 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TraitorEventPrefab : EventPrefab + { + class MissionRequirement + { + public Identifier MissionIdentifier; + public Identifier MissionTag; + public MissionType MissionType; + + public MissionRequirement(XElement element, TraitorEventPrefab prefab) + { + MissionIdentifier = element.GetAttributeIdentifier(nameof(MissionIdentifier), Identifier.Empty); + MissionTag = element.GetAttributeIdentifier(nameof(MissionTag), Identifier.Empty); + MissionType = element.GetAttributeEnum(nameof(MissionType), MissionType.None); + if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None) + { + DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}."); + } + } + + public bool Match(Mission mission) + { + if (mission == null) { return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None; } + if (!MissionIdentifier.IsEmpty) + { + return mission.Prefab.Identifier == MissionIdentifier; + } + else if (!MissionTag.IsEmpty) + { + return mission.Prefab.Tags.Contains(MissionTag); + } + else if (MissionType != MissionType.None) + { + return mission.Prefab.Type == MissionType; + } + return false; + } + } + + class LevelRequirement + { + private enum LevelType + { + LocationConnection, + Outpost, + Any + } + + private readonly LevelType levelType; + public ImmutableArray LocationTypes { get; } + + /// + /// Minimimum difficulty of the level for this event to get selected. Defaults to 0. + /// + private readonly float minDifficulty; + /// + /// Minimimum difficulty of the level for this event to get selected. Defaults to 5 or , whichever is lower. + /// + private readonly float minDifficultyInCampaign; + + //feels a little weird to have something this specific here, but couldn't think of a better way to implement this + public ImmutableArray RequiredItemConditionals; + + public LevelRequirement(XElement element, TraitorEventPrefab prefab) + { + levelType = element.GetAttributeEnum(nameof(LevelType), LevelType.Any); + LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); + minDifficulty = element.GetAttributeFloat(nameof(minDifficulty), 0.0f); + minDifficultyInCampaign = element.GetAttributeFloat(nameof(minDifficultyInCampaign), Math.Max(minDifficulty, 5.0f)); + List requiredItemConditionals = new List(); + foreach (var subElement in element.Elements()) + { + if (subElement.NameAsIdentifier() == "itemconditional") + { + requiredItemConditionals.AddRange(PropertyConditional.FromXElement(subElement)); + } + } + RequiredItemConditionals = requiredItemConditionals.ToImmutableArray(); + } + + public bool Match(Level level) + { + if (level?.LevelData == null) { return false; } + switch (levelType) + { + case LevelType.LocationConnection: + if (level.LevelData.Type != LevelData.LevelType.LocationConnection) { return false; } + break; + case LevelType.Outpost: + if (level.LevelData.Type != LevelData.LevelType.Outpost) { return false; } + break; + } + if (GameMain.GameSession?.Campaign != null) + { + if (level.Difficulty < minDifficultyInCampaign) { return false; } + } + else + { + if (level.Difficulty < minDifficulty) { return false; } + } + if (level.StartLocation == null) + { + if (LocationTypes.Any()) { return false; } + } + else + { + if (LocationTypes.Any() && !LocationTypes.Contains(level.StartLocation.Type.Identifier)) { return false; } + } + if (RequiredItemConditionals.Any()) + { + bool matchFound = false; + foreach (var item in Item.ItemList) + { + if (RequiredItemConditionals.All(c => item.ConditionalMatches(c))) + { + matchFound = true; + break; + } + } + if (!matchFound) { return false; } + } + return true; + } + } + + class ReputationRequirement + { + public Identifier Faction; + public Identifier CompareToFaction; + public float CompareToValue; + + public readonly PropertyConditional.ComparisonOperatorType Operator; + + public ReputationRequirement(XElement element, TraitorEventPrefab prefab) + { + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + + string conditionStr = element.GetAttributeString("reputation", string.Empty); + + string[] splitString = conditionStr.Split(' '); + string value; + if (splitString.Length > 0) + { + //the first part of the string is the operator, skip it + value = string.Join(" ", splitString.Skip(1)); + } + else + { + DebugConsole.ThrowError( + $"{conditionStr} in {prefab.Identifier} is too short."+ + "It should start with an operator followed by a faction identifier or a floating point value."); + return; + } + Operator = PropertyConditional.GetComparisonOperatorType(splitString[0]); + + if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal)) + { + CompareToValue = floatVal; + } + else + { + CompareToFaction = value.ToIdentifier(); + } + } + + public bool Match(CampaignMode campaign) + { + var faction1 = campaign.GetFaction(Faction); + if (faction1 == null) + { + DebugConsole.ThrowError($"Could not find the faction {Faction}."); + return false; + } + if (!CompareToFaction.IsEmpty) + { + var faction2 = campaign.GetFaction(Faction); + if (faction2 == null) + { + DebugConsole.ThrowError($"Could not find the faction {CompareToFaction}."); + return false; + } + return PropertyConditional.CompareFloat(faction1.Reputation.Value, faction2.Reputation.Value, Operator); + } + return PropertyConditional.CompareFloat(faction1.Reputation.Value, CompareToValue, Operator); + } + } + + public readonly Sprite? Icon; + public readonly Color IconColor; + + public const int MinDangerLevel = 1; + public const int MaxDangerLevel = 3; + + public ImmutableHashSet Tags; + + private readonly ImmutableArray reputationRequirements; + private readonly ImmutableArray missionRequirements; + private readonly ImmutableArray levelRequirements; + public bool HasReputationRequirements => reputationRequirements.Any(); + public bool HasMissionRequirements => missionRequirements.Any(); + public bool HasLevelRequirements => levelRequirements.Any(); + + /// + /// An event with one of these tags must've been completed previously for this event to trigger. + /// + public ImmutableHashSet RequiredCompletedTags; + + public readonly int DangerLevel; + + /// + /// Minimum number of non-spectating human players on the server for the event to get selected. + /// + public readonly int MinPlayerCount; + + /// + /// Number of players to assign as a "secondary traitor". + /// If both this and are defined, this is treated as a minimum number of secondary traitors. + /// + public readonly int SecondaryTraitorAmount; + + /// + /// Percentage of players to assign as a "secondary traitor". + /// + public readonly float SecondaryTraitorPercentage; + + /// + /// Does accusing a secondary traitor count as correctly identifying the traitor? + /// + public readonly bool AllowAccusingSecondaryTraitor; + + /// + /// Money penalty if the crew votes a wrong player as the traitor + /// + public readonly int MoneyPenaltyForUnfoundedTraitorAccusation; + + /// + /// Is this event chainable, i.e. does the same traitor get another, higher-lvl one if they complete this one successfully? + /// + public readonly bool IsChainable; + + public readonly float StealPercentageOfExperience; + + public TraitorEventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) + : base(element, file, fallbackIdentifier) + { + DangerLevel = MathHelper.Clamp(element.GetAttributeInt(nameof(DangerLevel), MinDangerLevel), MinDangerLevel, MaxDangerLevel); + + MinPlayerCount = element.GetAttributeInt(nameof(MinPlayerCount), 0); + + SecondaryTraitorAmount = element.GetAttributeInt(nameof(SecondaryTraitorAmount), 0); + SecondaryTraitorPercentage = element.GetAttributeFloat(nameof(SecondaryTraitorPercentage), 0.0f); + + AllowAccusingSecondaryTraitor = element.GetAttributeBool(nameof(AllowAccusingSecondaryTraitor), true); + + MoneyPenaltyForUnfoundedTraitorAccusation = element.GetAttributeInt(nameof(MoneyPenaltyForUnfoundedTraitorAccusation), 100); + + Tags = element.GetAttributeIdentifierImmutableHashSet(nameof(Tags), ImmutableHashSet.Empty); + RequiredCompletedTags = element.GetAttributeIdentifierImmutableHashSet(nameof(RequiredCompletedTags), ImmutableHashSet.Empty); + + StealPercentageOfExperience = element.GetAttributeFloat(nameof(StealPercentageOfExperience), 0.0f); + + IsChainable = element.GetAttributeBool(nameof(IsChainable), true); + + List reputationRequirements = new List(); + List levelRequirements = new List(); + List missionRequirements = new List(); + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "reputationrequirement": + reputationRequirements.Add(new ReputationRequirement(subElement!, this)); + break; + case "missionrequirement": + missionRequirements.Add(new MissionRequirement(subElement!, this)); + break; + case "levelrequirement": + levelRequirements.Add(new LevelRequirement(subElement!, this)); + break; + case "icon": + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + break; + } + } + this.reputationRequirements = reputationRequirements.ToImmutableArray(); + this.levelRequirements = levelRequirements.ToImmutableArray(); + this.missionRequirements = missionRequirements.ToImmutableArray(); + } + + public bool ReputationRequirementsMet(CampaignMode? campaign) + { + if (campaign == null) + { + //no requirements in the campaign + return true; + } + foreach (ReputationRequirement requirement in reputationRequirements) + { + if (!requirement.Match(campaign)) { return false; } + } + return true; + } + public bool MissionRequirementsMet(GameSession? gameSession) + { + if (gameSession == null) { return false; } + foreach (MissionRequirement requirement in missionRequirements) + { + if (gameSession.Missions.None(m => requirement.Match(m))) { return false; } + } + return true; + } + public bool LevelRequirementsMet(Level? level) + { + if (level == null) { return false; } + //by default (if no requirements are specified) traitor events happen in LocationConnections. + if (levelRequirements.None() && level.Type != LevelData.LevelType.LocationConnection) + { + return false; + } + foreach (LevelRequirement requirement in levelRequirements) + { + if (!requirement.Match(level)) { return false; } + } + return true; + } + + public override void Dispose() + { + Icon?.Remove(); + } + + public override string ToString() + { + return $"{nameof(TraitorEventPrefab)} ({Identifier})"; + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs new file mode 100644 index 000000000..ffa848166 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs @@ -0,0 +1,49 @@ +#nullable enable + +using Barotrauma.Networking; +using System.Linq; + +namespace Barotrauma +{ + sealed partial class TraitorManager + { + public struct TraitorResults : INetSerializableStruct + { + [NetworkSerialize] + public byte VotedAsTraitorClientSessionId; + + [NetworkSerialize] + public bool VotedCorrectTraitor; + + [NetworkSerialize] + public bool ObjectiveSuccessful; + + [NetworkSerialize] + public int MoneyPenalty; + + [NetworkSerialize] + public Identifier TraitorEventIdentifier; + + public TraitorResults(Client? votedAsTraitor, TraitorEvent traitorEvent) + { + VotedAsTraitorClientSessionId = votedAsTraitor?.SessionId ?? 0; + VotedCorrectTraitor = votedAsTraitor == traitorEvent.Traitor; + if (traitorEvent.Prefab.AllowAccusingSecondaryTraitor && !VotedCorrectTraitor) + { + VotedCorrectTraitor = traitorEvent.SecondaryTraitors.Contains(votedAsTraitor); + } + ObjectiveSuccessful = traitorEvent.CurrentState == TraitorEvent.State.Completed; + MoneyPenalty = votedAsTraitor != null && !VotedCorrectTraitor ? + traitorEvent.Prefab.MoneyPenaltyForUnfoundedTraitorAccusation : + 0; + TraitorEventIdentifier = traitorEvent.Prefab.Identifier; + } + + public Client? GetTraitorClient() + { + int sessionId = VotedAsTraitorClientSessionId; + return GameMain.NetworkMember?.ConnectedClients?.FirstOrDefault(c => c.SessionId == sessionId); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs deleted file mode 100644 index a8e58b990..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace Barotrauma -{ - partial class TraitorMissionResult - { - public readonly string EndMessage; - - public readonly Identifier MissionIdentifier; - - public readonly bool Success; - - public readonly List Characters = new List(); - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index a7dedde89..2e78f5bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -7,7 +7,7 @@ namespace Barotrauma { public delegate void TaskDelegate(); - private class Task + private sealed class Task { public TaskDelegate Deleg; public ManualResetEvent Mre; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index 9bc971557..fbfe75287 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -2,6 +2,7 @@ using Barotrauma.IO; using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -98,6 +99,16 @@ namespace Barotrauma return new Md5Hash(hash); } + public static Md5Hash MergeHashes(IEnumerable hashes) + { + using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5); + foreach (var hash in hashes) + { + incrementalHash.AppendData(hash.ByteRepresentation); + } + return BytesAsHash(incrementalHash.GetHashAndReset()); + } + public static Md5Hash CalculateForBytes(byte[] bytes) { return new Md5Hash(bytes, calculate: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 112281e50..63e0d5af6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -41,6 +41,19 @@ namespace Barotrauma public Option Bind(Func> binder) where TType : notnull => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + public T Match(Func some, Func none) + => TryUnwrap(out T? selfValue) ? some(selfValue) : none(); + + public void Match(Action some, Action none) + { + if (TryUnwrap(out T? selfValue)) + { + some(selfValue); + return; + } + none(); + } + public T Fallback(T fallback) => TryUnwrap(out var v) ? v : fallback; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index f4adb8a06..dc80cf2f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.IO.Compression; using System.Linq; @@ -9,11 +10,14 @@ using System.Text.RegularExpressions; using Barotrauma.IO; using Microsoft.Xna.Framework; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { static class SaveUtil { + public const string GameSessionFileName = "gamesession.xml"; + private static readonly string LegacySaveFolder = Path.Combine("Data", "Saves"); private static readonly string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer"); @@ -39,8 +43,6 @@ namespace Barotrauma public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded"); - public delegate void ProgressDelegate(string sMessage); - public static string TempPath { #if SERVER @@ -72,7 +74,7 @@ namespace Barotrauma try { - GameMain.GameSession.Save(Path.Combine(TempPath, "gamesession.xml")); + GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName)); } catch (Exception e) { @@ -82,7 +84,7 @@ namespace Barotrauma try { - string mainSubPath = null; + string? mainSubPath = null; if (GameMain.GameSession.SubmarineInfo != null) { mainSubPath = Path.Combine(TempPath, GameMain.GameSession.SubmarineInfo.Name + ".sub"); @@ -115,7 +117,7 @@ namespace Barotrauma try { - CompressDirectory(TempPath, filePath, null); + CompressDirectory(TempPath, filePath); } catch (Exception e) { @@ -130,9 +132,9 @@ namespace Barotrauma Submarine.Unload(); GameMain.GameSession = null; DebugConsole.Log("Loading save file: " + filePath); - DecompressToDirectory(filePath, TempPath, null); + DecompressToDirectory(filePath, TempPath); - XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); + XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, GameSessionFileName)); if (doc == null) { return; } if (!IsSaveFileCompatible(doc)) @@ -149,40 +151,26 @@ namespace Barotrauma string subPath = Path.Combine(TempPath, saveDoc.Root.GetAttributeString("submarine", "")) + ".sub"; selectedSub = new SubmarineInfo(subPath); - List ownedSubmarines = null; - var ownedSubsElement = saveDoc.Root.Element("ownedsubmarines"); - if (ownedSubsElement != null) + List ownedSubmarines = new List(); + + var ownedSubsElement = saveDoc.Root?.Element("ownedsubmarines"); + if (ownedSubsElement == null) { return ownedSubmarines; } + + foreach (var subElement in ownedSubsElement.Elements()) { - ownedSubmarines = new List(); - foreach (var subElement in ownedSubsElement.Elements()) - { - string subName = subElement.GetAttributeString("name", ""); - string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); - ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); - } + string subName = subElement.GetAttributeString("name", ""); + string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); + ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); } return ownedSubmarines; } - public static XDocument LoadGameSessionDoc(string filePath) - { - DebugConsole.Log("Loading game session doc: " + filePath); - try - { - DecompressToDirectory(filePath, TempPath, null); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error decompressing " + filePath, e); - return null; - } + public static bool IsSaveFileCompatible(XDocument? saveDoc) + => IsSaveFileCompatible(saveDoc?.Root); - return XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); - } - - public static bool IsSaveFileCompatible(XDocument saveDoc) + public static bool IsSaveFileCompatible(XElement? saveDocRoot) { - if (saveDoc?.Root?.Attribute("version") == null) { return false; } + if (saveDocRoot?.Attribute("version") == null) { return false; } return true; } @@ -192,14 +180,13 @@ namespace Barotrauma { File.Delete(filePath); } - catch (Exception e) { DebugConsole.ThrowError("ERROR: deleting save file \"" + filePath + "\" failed.", e); } //deleting a multiplayer save file -> also delete character data - var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath)); + var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath) ?? ""); if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) || fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer))) @@ -286,12 +273,12 @@ namespace Barotrauma List saveInfos = new List(); foreach (string file in files) { - XDocument doc = LoadGameSessionDoc(file); - if (!includeInCompatible && !IsSaveFileCompatible(doc)) + var docRoot = ExtractGameSessionRootElementFromSaveFile(file); + if (!includeInCompatible && !IsSaveFileCompatible(docRoot)) { continue; } - if (doc?.Root == null) + if (docRoot == null) { saveInfos.Add(new CampaignMode.SaveInfo( FilePath: file, @@ -304,7 +291,7 @@ namespace Barotrauma List enabledContentPackageNames = new List(); //backwards compatibility - string enabledContentPackagePathsStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty); + string enabledContentPackagePathsStr = docRoot.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty); foreach (string packagePath in enabledContentPackagePathsStr.Split('|')) { if (string.IsNullOrEmpty(packagePath)) { continue; } @@ -312,7 +299,7 @@ namespace Barotrauma string fileName = Path.GetFileNameWithoutExtension(packagePath); if (fileName == "filelist") { - enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath))); + enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath) ?? "")); } else { @@ -320,7 +307,7 @@ namespace Barotrauma } } - string enabledContentPackageNamesStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty); + string enabledContentPackageNamesStr = docRoot.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty); //split on pipes, excluding pipes preceded by \ foreach (string packageName in Regex.Split(enabledContentPackageNamesStr, @"(? 255) + { + throw new Exception( + $"Failed to compress \"{sDir}\" (file name length > 255)."); + } + // File name length is encoded as a 32-bit little endian integer here + zipStream.WriteByte((byte)sRelativePath.Length); + zipStream.WriteByte(0); + zipStream.WriteByte(0); + zipStream.WriteByte(0); + // File name content is encoded as little-endian UTF-16 + var strBytes = Encoding.Unicode.GetBytes(sRelativePath.CleanUpPathCrossPlatform(correctFilenameCase: false)); + zipStream.Write(strBytes, 0, strBytes.Length); //Compress file content byte[] bytes = File.ReadAllBytes(Path.Combine(sDir, sRelativePath)); @@ -402,25 +397,26 @@ namespace Barotrauma zipStream.Write(bytes, 0, bytes.Length); } - public static void CompressDirectory(string sInDir, string sOutFile, ProgressDelegate progress) + public static void CompressDirectory(string sInDir, string sOutFile) { IEnumerable sFiles = Directory.GetFiles(sInDir, "*.*", System.IO.SearchOption.AllDirectories); - int iDirLen = sInDir[sInDir.Length - 1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; + int iDirLen = sInDir[^1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; - using (FileStream outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)) - using (GZipStream str = new GZipStream(outFile, CompressionMode.Compress)) - foreach (string sFilePath in sFiles) - { - string sRelativePath = sFilePath.Substring(iDirLen); - progress?.Invoke(sRelativePath); - CompressFile(sInDir, sRelativePath, str); - } + using var outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write) + ?? throw new Exception($"Failed to create file \"{sOutFile}\""); + using GZipStream str = new GZipStream(outFile, CompressionMode.Compress); + foreach (string sFilePath in sFiles) + { + string sRelativePath = sFilePath.Substring(iDirLen); + CompressFile(sInDir, sRelativePath, str); + } } public static System.IO.Stream DecompressFileToStream(string fileName) { - using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read); + using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read) + ?? throw new Exception($"Failed to open file \"{fileName}\""); System.IO.MemoryStream streamToReturn = new System.IO.MemoryStream(); using GZipStream gzipStream = new GZipStream(originalFileStream, CompressionMode.Decompress); @@ -443,10 +439,11 @@ namespace Barotrauma return fileDirFull.StartsWith(rootDirFull, StringComparison.OrdinalIgnoreCase); } - - private static bool DecompressFile(bool writeFile, string sDir, System.IO.BinaryReader reader, ProgressDelegate progress, out string fileName) + + private static bool DecompressFile(System.IO.BinaryReader reader, [NotNullWhen(returnValue: true)]out string? fileName, [NotNullWhen(returnValue: true)]out byte[]? fileContent) { fileName = null; + fileContent = null; if (reader.PeekChar() < 0) { return false; } @@ -455,7 +452,7 @@ namespace Barotrauma if (nameLen > 255) { throw new Exception( - $"Failed to decompress \"{sDir}\" (file name length > 255). The file may be corrupted."); + $"Failed to decompress (file name length > 255). The file may be corrupted."); } byte[] strBytes = reader.ReadBytes(nameLen * sizeof(char)); @@ -463,57 +460,40 @@ namespace Barotrauma .Replace('\\', '/'); fileName = sFileName; - progress?.Invoke(sFileName); //Decompress file content int contentLen = reader.ReadInt32(); - byte[] contentBytes = reader.ReadBytes(contentLen); + fileContent = reader.ReadBytes(contentLen); - string sFilePath = Path.Combine(sDir, sFileName); - string sFinalDir = Path.GetDirectoryName(sFilePath); - - if (!IsExtractionPathValid(sDir, sFinalDir)) - { - throw new InvalidOperationException( - $"Error extracting \"{sFileName}\": cannot be extracted to parent directory"); - } - - if (!writeFile) { return true; } - - Directory.CreateDirectory(sFinalDir); - int maxRetries = 4; - for (int i = 0; i <= maxRetries; i++) - { - try - { - using (FileStream outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)) - { - outFile.Write(contentBytes, 0, contentLen); - } - break; - } - catch (System.IO.IOException e) - { - if (i >= maxRetries || !File.Exists(sFilePath)) { throw; } - DebugConsole.NewMessage("Failed decompress file \"" + sFilePath + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); - Thread.Sleep(250); - } - } return true; } - public static void DecompressToDirectory(string sCompressedFile, string sDir, ProgressDelegate progress) + public static void DecompressToDirectory(string sCompressedFile, string sDir) { DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "..."); - int maxRetries = 4; + const int maxRetries = 4; for (int i = 0; i <= maxRetries; i++) { try { - using (var memStream = DecompressFileToStream(sCompressedFile)) - using (System.IO.BinaryReader reader = new System.IO.BinaryReader(memStream)) - while (DecompressFile(true, sDir, reader, progress, out _)) { }; + using var memStream = DecompressFileToStream(sCompressedFile); + using var reader = new System.IO.BinaryReader(memStream); + while (DecompressFile(reader, out var fileName, out var contentBytes)) + { + string sFilePath = Path.Combine(sDir, fileName); + string sFinalDir = Path.GetDirectoryName(sFilePath) ?? ""; + if (!IsExtractionPathValid(sDir, sFinalDir)) + { + throw new InvalidOperationException( + $"Error extracting \"{fileName}\": cannot be extracted to parent directory"); + } + + Directory.CreateDirectory(sFinalDir); + using var outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write) + ?? throw new Exception($"Failed to create file \"{sFilePath}\""); + outFile.Write(contentBytes, 0, contentBytes.Length); + } break; } catch (System.IO.IOException e) @@ -527,18 +507,20 @@ namespace Barotrauma public static IEnumerable EnumerateContainedFiles(string sCompressedFile) { - int maxRetries = 4; + const int maxRetries = 4; HashSet paths = new HashSet(); for (int i = 0; i <= maxRetries; i++) { try { - using (var memStream = DecompressFileToStream(sCompressedFile)) - using (System.IO.BinaryReader reader = new System.IO.BinaryReader(memStream)) - while (DecompressFile(false, "", reader, null, out string fileName)) - { - paths.Add(fileName); - } + paths.Clear(); + using var memStream = DecompressFileToStream(sCompressedFile); + using var reader = new System.IO.BinaryReader(memStream); + while (DecompressFile(reader, out var fileName, out _)) + { + paths.Add(fileName); + } + break; } catch (System.IO.IOException e) { @@ -554,44 +536,96 @@ namespace Barotrauma return paths; } - public static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs, bool overwriteExisting = false) + /// + /// Extracts the save file (including all the subs in it) to a temporary folder and returns the game session document. + /// If you only need the gamesession doc, use instead. + /// + /// + /// + public static XDocument? DecompressSaveAndLoadGameSessionDoc(string savePath) { - // Get the subdirectories for the specified directory. - DirectoryInfo dir = new DirectoryInfo(sourceDirName); - - if (!dir.Exists) + DebugConsole.Log("Loading game session doc: " + savePath); + try { - throw new System.IO.DirectoryNotFoundException( - "Source directory does not exist or could not be found: " - + sourceDirName); + DecompressToDirectory(savePath, TempPath); } - - IEnumerable dirs = dir.GetDirectories(); - // If the destination directory doesn't exist, create it. - if (!Directory.Exists(destDirName)) + catch (Exception e) { - Directory.CreateDirectory(destDirName); + DebugConsole.ThrowError("Error decompressing " + savePath, e); + return null; } + return XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); + } - // Get the files in the directory and copy them to the new location. - IEnumerable files = dir.GetFiles(); - foreach (FileInfo file in files) + /// + /// Extract *only* the root element of the gamesession.xml file in the given save. + /// For performance reasons, none of its child elements are returned. + /// + public static XElement? ExtractGameSessionRootElementFromSaveFile(string savePath) + { + const int maxRetries = 4; + for (int i = 0; i <= maxRetries; i++) { - string tempPath = Path.Combine(destDirName, file.Name); - if (!overwriteExisting && File.Exists(tempPath)) { continue; } - file.CopyTo(tempPath, true); - } - - // If copying subdirectories, copy them and their contents to new location. - if (copySubDirs) - { - foreach (DirectoryInfo subdir in dirs) + try { - string tempPath = Path.Combine(destDirName, subdir.Name); - CopyFolder(subdir.FullName, tempPath, copySubDirs, overwriteExisting); + using var memStream = DecompressFileToStream(savePath); + using var reader = new System.IO.BinaryReader(memStream); + while (DecompressFile(reader, out var fileName, out var fileContent)) + { + if (fileName != GameSessionFileName) { continue; } + + // Found the file! Here's a quick byte-wise parser to find the root element + int tagOpenerStartIndex = -1; + for (int j = 0; j < fileContent.Length; j++) + { + if (fileContent[j] == '<') + { + // Found a tag opener: return null if we had already found one + if (tagOpenerStartIndex >= 0) { return null; } + tagOpenerStartIndex = j; + } + else if (j > 0 && fileContent[j] == '?' && fileContent[j - 1] == '<') + { + // Found the XML version element, skip this + tagOpenerStartIndex = -1; + } + else if (fileContent[j] == '>') + { + // Found a tag closer, if we know where the tag opener is then we've found the root element + if (tagOpenerStartIndex < 0) { continue; } + + string elemStr = Encoding.UTF8.GetString(fileContent.AsSpan()[tagOpenerStartIndex..j]) + "/>"; + try + { + return XElement.Parse(elemStr); + } + catch (Exception e) + { + DebugConsole.NewMessage( + $"Failed to parse gamesession root in \"{savePath}\": {{{e.Message}}}.", + Color.Red); + // Parsing the element failed! Return null instead of crashing here + return null; + } + } + } + } + break; + } + catch (System.IO.IOException e) + { + if (i >= maxRetries || !File.Exists(savePath)) { throw; } + + DebugConsole.NewMessage( + $"Failed to decompress file \"{savePath}\" for root extraction {{{e.Message}}}, retrying in 250 ms...", + Color.Red); + Thread.Sleep(250); } } + + return null; } + public static void DeleteDownloadedSubs() { if (Directory.Exists(SubmarineDownloadFolder)) @@ -614,7 +648,7 @@ namespace Barotrauma } } - public static void ClearFolder(string folderName, string[] ignoredFileNames = null) + public static void ClearFolder(string folderName, string[]? ignoredFileNames = null) { DirectoryInfo dir = new DirectoryInfo(folderName); @@ -640,7 +674,7 @@ namespace Barotrauma foreach (DirectoryInfo di in dir.GetDirectories()) { ClearFolder(di.FullName, ignoredFileNames); - int maxRetries = 4; + const int maxRetries = 4; for (int i = 0; i <= maxRetries; i++) { try diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 9cbc56614..9605361b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -27,6 +27,31 @@ namespace Barotrauma } } + internal readonly record struct SquareLine(Vector2[] Points, SquareLine.LineType Type) + { + internal enum LineType + { + /// + /// Normal 4 point line + /// + /// + /// ┏━━━ end + /// ┃ + /// start ━━━┛ + /// + FourPointForwardsLine, + /// + /// A line where the end is behind the start and 2 extra points are used to draw it + /// + /// + /// start ━┓ + /// ┏━━━━━━┛ + /// ┗━ end + /// + SixPointBackwardsLine + } + } + static partial class ToolBox { public static bool IsProperFilenameCase(string filename) @@ -396,6 +421,10 @@ namespace Barotrauma public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) { + if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T))) + { + objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0); + } List objectList = objects.ToList(); List weights = objectList.Select(o => weightMethod(o)).ToList(); return SelectWeightedRandom(objectList, weights, random); @@ -408,7 +437,7 @@ namespace Barotrauma public static T SelectWeightedRandom(IList objects, IList weights, Random random) { - if (objects.Count == 0) { return default(T); } + if (objects.Count == 0) { return default; } if (objects.Count != weights.Count) { @@ -427,7 +456,7 @@ namespace Barotrauma } randomNum -= weights[i]; } - return default(T); + return default; } public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5) @@ -806,5 +835,43 @@ namespace Barotrauma if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); } return self.Equals(other); } + + public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end, float knobLength = 24f) + { + Vector2[] points = new Vector2[6]; + + // set the start and end points + points[0] = points[1] = points[2] = start; + points[5] = points[4] = points[3] = end; + + points[2].X += (points[3].X - points[2].X) / 2; + points[2].X = Math.Max(points[2].X, points[0].X + knobLength); + points[3].X = points[2].X; + + bool isBehind = false; + + // if the node is "behind" us do some magic to make the line curve to prevent overlapping + if (points[2].X <= points[0].X + knobLength) + { + isBehind = true; + points[1].X += knobLength; + points[2].X = points[2].X; + points[2].Y += (points[4].Y - points[1].Y) / 2; + } + + if (points[3].X >= points[5].X - knobLength) + { + isBehind = true; + points[4].X -= knobLength; + points[3].X = points[4].X; + points[3].Y -= points[3].Y - points[2].Y; + } + + SquareLine.LineType type = isBehind + ? SquareLine.LineType.SixPointBackwardsLine + : SquareLine.LineType.FourPointForwardsLine; + + return new SquareLine(points, type); + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 75b4957f2..44eaec1b1 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,290 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.1.14.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Traitor overhaul: +- Completely redesigned traitor system, which also now works in the campaign mode. +- The traitor missions (now called "events") have been implemented using the scripted event system, and can be created in the event editor. We unfortunately had to drop support for the old system; even though we aim to not do any backwards-compatibility breaking changes now that we're past 1.0, after evaluating the number of mods that include custom traitor missions and seeing how few there are, we felt that it's better to depreciate the old system instead of maintaining two separate systems. We're sorry for the inconvenience, but we're hopeful the move to the scripted event system will allow modders to create more complex traitor events much more easily! +- A total of 27 completely new traitor events. +- Multi-traitor events and code words are back! +- Instead of the yes/no/maybe traitor setting, you can choose the probability of a traitor event per round (e.g. 50% = a traitor event happens on half the rounds on average). +- Each event has a "danger level" that describes how destructive/dangerous the event is. You can choose how dangerous events can get chosen in the server lobby, for example if you don't want very destructive traitor objectives in the campaign mode. +- If a traitor completes their objective successfully, they may get assigned a more dangerous objective on future rounds (up until the maximum defined danger level is reached). +- Players who haven't been traitors before/recently have a higher chance of getting selected as traitors. +- In the campaign mode, completing a traitor objective gives the traitors rewards such as getting to "steal" a portion of the mission experience. +- The players can vote which player they suspect as the traitor. At the end of the round, if at least half of the players have voted for the same player, that player will be accused as a traitor. If the accusation is correct, the traitor's objective will fail and they will receive no rewards. If the accusation is incorrect, the crew will receive a monetary penalty. +- Added various new traitor items, perhaps most notably the Radio Jammer, a handheld battery-powered device that temporarily disables radio communications (both text and voice chat). + +Stacking and storage changes: +- Added Backpack, essentially a larger variant of the toolbelt with a small speed debuff. Can be obtained through the new assistant talent "Bag It Up", and purchased in stores around mid-way through the campaign. +- Oxygen and fuel tanks no longer stack in player inventories, but their capacity has been increased to compensate. A stack of oxygen tanks would last nearly 45 minutes (and that's not even accounting for high-quality tanks), which meant you could easily carry so much oxygen that running out is practically never a risk. +- Increased stack sizes of most items (such as materials, ammo, meds) to 32. +- Increased cabinet capacities. +- Storage containers can no longer be put in normal cabinet slots. The intention is to prevent being able to use storage containers as a way to significantly increase cabinet capacity (all the way up to 2880!). The larger stack sizes and increased cabinet capacities still allow storing a very large number of items, without having to resort to storage containers which make managing the items in the cabinet more inconvenient. +- Cabinets now have a handful of special "extra slots" specifically for storage containers. Storage containers have other uses besides just increasing storage capacity, and these extra slots are intended to keep them a viable option for those other uses. +- Added buttons for merging stacks and alphabetical sorting to all cabinets. +- Dropped stacks of items behave as one physical object and can be picked up by clicking once, instead of the stack becoming a bunch of individual items that need to be picked up one-by-one. +- Restricted the maximum stack sizes of character inventories and holdable/wearable containers to 8. Meaning you can carry 8 items per slot, but cabinets and crates can fit the "full" x32 stacks. + +Talents: +- Fixed "By the Book" giving you the bonuses regardless if you complete a mission or not. +- Changed how Scavenger talent works: previously it had a chance of doubling the loot in a container, which often led to excessive amounts of loot (and other times, no extra loot at all). Now the chance works per-item instead. +- Fixed nuclear shells and depth charges fabricated with the "Nuclear Option" talent providing incendium when deconstructed, even though it's not used in the fabrication recipe. +- Fixed having one captain with the "Leading by Example" talent and another with "Family" messing up the afflictions applied by them (one trying to apply "Excellent Morale" and the other trying to remove it). + +Misc changes and additions: +- Added Circuit Box, an item that allows creating circuits without having to fabricate and place each component individually on the sub's walls. +- Added lights that indicate the ammo status to turret loaders. +- Characters "grab" the device/item they're interacting with, making it easier to see e.g. whether a character is taking something from a cabinet or just standing next to it. +- Allowed any type of ammo box to be used in the turret ammo box recycling recipes. +- Improved explosion particle effects. +- Added a new abandoned outpost mission variant (relating to one of the shadier factions). +- Pets become unhappy faster. Previously the happiness decrease rate was so low the pets were more likely to starve to death than to become unhappy. +- Pets no longer regenerate health when they're well-fed. This was a leftover from the 1st implementation of the pets, and is no longer necessary, because they now regenerate health while eating. +- Repositioned item interfaces retain their positions between rounds. +- Killing/handcuffing the terrorists during escort missions is no longer required to complete the mission. Fixes other means of detaining them (such as brigging, stunning or paralyzing) not being considered valid before. +- It's now possible to access the gene splicer slot of dead/incapacitated characters. +- Increased fruits' impact tolerances to prevent them from breaking when falling. +- Tweaks to the sound effects when under high pressure. +- Underwater scooter's light turns on when aiming (not just when actually using the scooter). +- Captain's pipe and cigar are used by holding RMB (not LMB+RMB) to make their usage consistent with other "consumables". +- Added broken state sprites for Thalamus' fleshspike and fleshgun. Adjusted the fleshgun particle effect. +- Made characters only flip from one side of a ladder to another when aiming. Makes it easier to interact with things next to the ladder, when the character doesn't automatically flip to the "wrong side" when you try to highlight something. +- Beacon station and cave markers are removed from the sonar once the beacon/cave mission is completed. +- The Output and FalseOutput of switches and periscopes are editable. +- Added a "disable_output" pin to supercapacitors and batteries. Particularly useful for auto-operated turrets, as it can be used to power them down. +- Made auto-operated turrets much better at using pulse lasers, chainguns and other turrets that need to wind up before firing. Previously the AI was too eager to switch between targets, as opposed to waiting for the turret to wind up and actually fire. +- Added "activate_out" output to doors (outputs a signal when someone toggles the door) + an option to make the door NOT open/close when someone tries to toggle it. Can be used to run a signal through some external circuit when someone tries to open a door with integrated buttons. + +Balance: +- Adjusted ammo fabrication recipes to fix lead being too scarce (considering it's needed for almost all ammo types). Now ammunition can be fabricated from different materials, not just lead. +- Revisited the contents, the difficulty, and the rewards of monster missions. +- Balanced reputation gains from missions: it should now be less easy to maintain a positive reputation with all factions. +- Slightly increased character impact damage (i.e. damage taken when you fall or get thrown against something). +- Being under high pressure causes organ damage. Without this, it's possible to basically ignore high pressure as long as you're quick (e.g. you can briefly go outside or to a breached ballast to fix walls). +- Made gravity sphere, guardiansteamcannon and guardianbeamweapon cause a bit of radiation sickness. Afflictions that can't be treated with morphine + bandages should be more common, so here's some! +- Piezo crystals cause a bit of stun and burns on characters. +- Nerfed fractalguardian_emp a bit: longer attack cooldown, shorted EMP range. The previous values made it too easy to get softlocked. +- Added 50% flow resistance to makeshift armor to make it more effective in indoors combat. +- Revisited the effects of liquid oxygenite and the oxygen related effects of deusizine to keep them balanced. Reduced the required skill for applying liquid oxygenite to compensate for the failure effects. +- Reduced Mudraptor's priority of eating dead bodies with the intention of making it not as easy to distract them killing one in a pack. +- Improved the monster nest mission progression by introducing harder variants of all the missions. Adjusted the existing nest missions a bit. +- Buffed the ancient weapon: increase the burn damage from 40 to 50, make them ignore 50% of armor protection, allow them to cut off limbs and break (some) armor. Add stun for stopping power. +- Defined fire damage (on ballast flora) and AI combat priority for the ancient weapon. +- Set the treatmentthreshold for oxygen loss to 100, so that the bots won't try to treat patients so eagerly. Now they should react only slightly before the target faints. Note that this also affects when the suitable treatments are shown in the health interface. + +Optimization: +- Optimizations to situations when there's lots of NPCs nearby. Particularly noticeable in very crowded outposts. +- Optimized the Load Game menu in both singleplayer and multiplayer, dramatically reducing the time the game freezes when opening it. +- Optimized flashlights and other directional lights. +- Loading optimizations (loading screens should now be noticeably quicker). +- Thalamus: spawn the initial cells when the AI is loaded (at the round start) instead of doing that when the player is close by. Gets rid of the notable lag spike when approaching a Thalamus infested wreck. +- Miscellaneous smaller optimizations. + +Submarines: +- Reworked Berilia. +- Reworked R-29. +- Linked fabricators to the cabinets next to them in all vanilla subs. +- Various fixes and improvements to Orca. +- Fixed waypoints outside the submarine not being disabled as obstructed when the connection doesn't overlap a wall. +- Waypoint fixes to most player subs. +- Fixes to transferring items between subs: some items that didn't fit into cabinets weren't put into crates even if they could've been, and some items spawned unnecessary crates because they were configured to spawn in a crate despite being too big to put in one. +- Fixed incorrect "low on fuel" warning when switching to a new sub with no fuel, even if you have fuel in your current sub and opted to transfer them to the new one. + +Multiplayer: +- The skill loss on death can be adjusted in the server settings. Defaults to 50% (previously 75%). +- Fixed characters keeping all the injuries they've received while being "braindead" (killed due to disconnecting), meaning if you for example hack the character to pieces while braindead, and the character respawns next round, they'll spawn dead. +- If a client joins when their character is "braindead" (killed due to disconnection), the character is now revived and the client immediately regains control of it. But only if the character's vitality is above 0 - if they have received other lethal injuries or despawned, they'll have to wait for a "normal" respawn. +- Fixed icon in server details panel in the server list saying that every server is modded. +- Fixed clients spawning as characters even if they've opted to spectate if the game mode is switched from campaign to some other mode and back. +- Fixed fabricator sometimes starting to fabricate an incorrect amount if you click the "start" button quickly after adjusting the amount. +- Fixed occasional "received invalid SetAttackTarget message" errors in multiplayer. Happened when some attack caused the target to be killed and despawned immediately (e.g. when a pet was instakilled by a monster). +- Fixed inability to attach items to walls outside the sub in MP. +- The outpost manager is always killable in the Tormsdale report event even if the killing outpost NPCs is disallowed on the server. +- Fixed certain kinds of network messages being sent using an unreliable delivery method (potentially causing occasional issues with file transfers and traitor messages). +- Fixed inability to choose or vote for a sub if you don't have all the content packages required for the sub. +- Fixed clients not seeing the votes of anyone who voted before them on the ready check. +- Fixed dropping off ladders when you open your own health interface in multiplayer. +- Fixed changing character appearance triggering the "wait X seconds until you can rename again" warning. +- Fixed replaced shuttles not getting repaired client-side when you purchase the replacement, making it look like you can't repair them (because they're already repaired server-side). +- Fixed handheld sonar beacon's textbox becoming empty when you enter something in it in multiplayer. +- Fixed "Attempted to create a network event for an item That hasn't been fully initialized yet" console error when spawning a sonar beacon mid-round. +- Fixed characters getting removed at the end of the round if they've died and then been revived with the "revive" console command. + +AI: +- Fixed searchlights not attracting monsters. +- Fixed bots "cleaning up" (or stealing) batteries from portable pumps. +- Fixed bots never cleaning up detached wires. +- Fixed bots sometimes ignoring the leaks right next to doors/hatches. +- Fixed bots cleaning up items that are being eaten by a pet. +- Monsters not ignore provocative items in the inventories of characters they're configured to ignore (with the targeting state "Idle"). Fixes husks attacking characters that are wearing cultist robes with a diving suit. +- Autopilot now avoids the floating ice chunks using the same logic it uses to avoid ice spires. +- Defined AI combat priority for the alien pistol. +- Adjusted the combat priorities of flamer and steam prototype gun. +- Fixed bots sometimes drowning in the ballast because they didn't think they'd need diving gear if the ballast is not full (but still has enough water to drown). +- Fixed bots sometimes preferring to get a new weapon instead of reloading their current one, even if they had easy access to more ammo. +- Fixed bots accepting orders but not doing anything while handcuffed. The bots will now refuse to do things that they need hands when they are handcuffed. +- Fixed bots still sometimes getting stuck on corners around staircases. +- Bots don't attempt to repair or fix leaks in non-friendly subs (e.g. wrecks or ruins). +- Bots don't attempt to clean items in the main sub in outpost levels. +- Fixed bots sometimes just swimming around when they should be able to get back to the sub. +- Fixed orders persisting even if the target no longer exists after a sub switch (e.g. a bot might attempt to operate a turret that doesn't exist, resulting in them doing nothing). +- Fixed "follow" order not always persisting between rounds. +- Bots don't try to use stabilozine to treat deliriumine poisoning. It only slows down the progress of the poisoning, which is kind of pointless in the case of the non-lethal deliriumine poisoning. + +Alien ruins and artifacts: +- The ruins now start smaller and grow bigger gradually, depending on the difficulty level. +- Ruins start to appear a little later in the campaign (in the second biome). +- Initially only "scan ruin" missions are given, "ruin salvage" and "clear ruin" missions don't appear until later. +- Adjusted the loot distribution in the ruins. +- Fixed guardians and defensebot(?) spawning the blood decal when they get dismembered. +- Fixed guardian pod damage sounds sometimes being inaudible. +- Artifact transport cases require batteries to nullify the effects of the artifacts. The batteries last a little over 8 minutes. Also added some animations and lights to the case when powered. +- Made Faraday Artifact's EMP effect a little stronger, and added a discharge coil effect out of water. +- Thermal Artifact now emits steam in water that burns and damages walls. +- Nasonov Artifact now also disrupts the sonar. Added particle effects. +- Psychosis Artifact now generates watcher's gaze periodically, causing psychosis/nausea and strengthening nearby monsters. +- Sky Artifact now drains water on top of draining oxygen, making them a little more tricky to deal with. +- Doubled the price of all artifacts. +- Fixed artifacts never spawning as random loot in ruins. + +Fixes: +- Fixed 2nd end level being impossible to complete if you've already completed the campaign once, because the logbooks didn't respawn on the 2nd loop. +- Fixed inability to equip one-handed items when you're holding a one-handed container that item can go inside (e.g. trying to equip a revolver while holding a storage container would just put it in the container). +- Fixed an exploit that allowed giving your ID card access to places it shouldn't have access to (by placing it in the inventory of e.g. a bot captain and saving and reloading). +- Fixed mollusk and skitter genes giving much less vigor/hyperactivity than intended, making them practically useless. +- Fixed crashing if you change the language when there's scrolling texts (such as "your skills might be insufficient...") visible on the screen. +- Fixed an exploit that allowed using beds/chairs and crouching to trigger a sort of "noclip". +- Fixed particle "z-fighting" (particles flickering in front of each other). Was particularly noticeable with smoke particles. +- Fixed lighting and LOS behaving strangely on very long walls (not extruding far enough to cover the whole screen, allowing you to see through the wall). +- Fixed rifle reload sounds sometimes not playing if you fire in very rapid succession. +- Fixed files listed in file dialogs (such as when choosing a thumbnail for a mod) being in an inconsistent order on Linux. +- Fixed gaps generating strangely on "Shuttle Shell A Glass A". +- Fixed autoturrets being unable to fire inside hulls. +- Fixed “badvibrations3” event (last part of the event chain where you're assigned to salvage a psychosis artifact) never triggering. +- Fixed "sleight of hand" event not giving you the trinket if you choose the 1st dialog option. +- Fixed husk symbiosis getting removed from characters after finishing the campaign. +- Fixed heavy wrench not stunning unskilled users like it should. +- Fixed Jovian radiation no longer causing any noticeable radiation sickness, because it healed faster than the Jovian radiation causes it. +- Fixed chaingun's "winding up" sound not following the sub. +- Only allow putting syringes in autoinjectors (autoinjecting bandages doesn't make much sense, and doesn't work properly). +- Fixed submarines getting stopped by the collider of the acid mist emitted by acid grenades. +- Fixed connection panel layout not working properly on very large resolutions (pins being excessively large and/or going below the borders of the panel's frame). +- Fixed an issue in the "toy hammer" task given by the Jestmaster. The event would never complete, and instead just endlessly increase your reputation when hitting the monster with the hammer. +- Fixed occasional "no suitable return target found" console errors when leaving a location. +- Fixed explosive slugs exploding in the barrel when shot into the void in the sub editor's test mode. +- Fixed attaching items in-game not aligning them with the grid the same way as in the sub editor (offsetting them by half the size of the grid). +- Fixed event trigger icon (the yellow exclamation mark) hiding the conversation icon when an event targets the outpost manager. +- Fixed husks sometimes moving at an unintendedly slow speed on land, making the walking animation look strange (sort of sliding while barely moving their feet). +- Fixed text display's light turning off when copying one in the sub editor. +- Fixed 40mm grenades not exploding in nuclear shells. +- Fixed bringing the cursor over the inventory interrupting using tools and weapons. +- Minerals can't be grabbed before they've been detached with a plasma cutter (it was confusing to be able to grab the item and have the "detaching" progress bar appear, despite it doing nothing). +- Fixed sonar not displaying markers for other subs in the sub editor test mode. +- Fixed fabricator UI not refreshing the skill/time requirements when selecting a linked cabinet instead of the fabricator itself. +- Fixed fabricator not being able to pull materials from the user's inventory if the user selected a linked cabinet instead of the fabricator itself. +- Fixed submarine preview highlighting hulls that have the same name even if they're not linked or near each other. +- Fixed rope pull force originating from the item's origin instead of the barrel. +- Improved how electrical discharge coils determine which walls are outer walls: use the actual position of the structure's body, and check if there's hulls at either side of it. +- Fixed submarine wall "shell a combo 1 1" being incorrectly configured as a horizontal wall. Caused EDC to behave strangely on these walls. +- Fixed monsters sometimes spawning within the player's sight in wrecks (or ruins). +- Fixed corpses carried to the sub from a wreck just disappearing at the end of the round along with the items in their inventory. +- Fixed infinite loop in Character.CanInteractWith if the target item is in a container it's linked to, and has been configured to be displayed side-by-side. For example, if you place a cabinet linked to a fabricator inside the fabricator. +- Fixed incorrect type of airlock module being sometimes used in levels between outposts (e.g. a colony airlock module even though the outpost is a normal one). +- Fixed 40mm acid grenades sometimes appearing to explode in the grenade launcher client-side. +- Fixed nuclear reactor's explosion effects sometimes not being visible client-side. +- Fixed holdable item's sprite origin not getting mirrored on the x-axis. There was a previous attempt at fixing this, but it didn't work correctly: it adjusted the position where the item is held, in a way that made the sprite appear at the correct position. The sprite origin shouldn't affect the position of the item's body though, just where the sprite is drawn relative to the body. The previous fix also didn't take flipping into account, causing the body to be "the wrong way around" when facing left. +- Turn the reactor auto temp on at round end, if the reactor is actively managed by a bot. Fixes reactor sometimes generating too much power at the beginning of the rounds. Only happens later in the game when the bot operating the reactor is skilled enough to manually manage the reactor. +- Fixed thalamus being allowed to load the cheap nuclear shells (talent item) when it loads the railgun. +- Fixed a rounding error making some hairs/attachments in mods impossible to select. +- Fixed some properties of wearable InheritLimbDepth, InheritScale, InheritSourceRect, InheritOrigin being forced to true, and ObscureOtherWearables being forced to None on all wearable sprites except items regardless of what's set in XML. +- Fixed repair tools hitting severed (or hidden) limbs, which sometimes caused the target to not take damage. +- Fixed ancient weapons not being able to cut minerals. +- Fixed "hidden in game" entities + propeller's damage area indicator being visible in images generated with "wikiimage_sub". +- Fixed gaps that aren't linked to anything (e.g. gaps fully inside a hull) counting as hull breaches on the status monitor. +- Fixed contained items in a held item (e.g. flashlight in a rifle, harpoons in a harpoon gun) rotating "choppily". +- Fixed particle "z-fighting" (flickering in front of each other). Was particularly noticeable with alpha-blended particles like smoke. +- Fixed damage from structure damage "shrapnel explosions" being very inconsistent. The explosion didn't ignore obstacles, meaning any structures between the explosion and the character (including the wall the shrapnel comes from) would drastically reduce the damage. This meant the damage was usually very low, but with specific kinds of wall configurations could be enough to instakill a character. +- Fixed characters letting go of the character they're dragging when moving out from or into a sub. +- Changed the formula that converts the rectangular steering vector on the nav terminal to circular. The previous formula caused the "steering arrow" to point at a slightly different direction than where the submarine was actually trying to head to. Most noticeable when controlling turrets using the nav terminal. +- Fixed diving masks reducing movement speed. +- Fixed water in docked shuttles/drones not affecting the buoyancy of the main submarine as much as it should. +- Fixed sub editor sometimes deleting previously selected entities when deleting something using the right-click context menu. +- Fixed monsters sometimes spawning inside the sub after you restore a beacon station. +- Fixed trying to return back from a "deadend location" with no outpost sometimes not working. +- Fixed fractal guardians damaging themselves with their steam cannons. +- Fixed a rare crash caused by an exception in Explosion.RangedStructureDamage. Happened when structure damage triggered an explosion, and said explosion caused another explosion that caused more structure damage. Could happen e.g. when the shrapnel from a breach kills a terminal cell, which explodes and causes more structure damage. +- Fixed auto-operated turrets being able to function without power. +- Fixed it being possible for the last location before the end levels to turn into an outpost if you manage to spread habitation all the way there. +- Changed sonar flora sprite depths to prevent them from being obscured by the edge chunk objects. +- Fixed some headgear (such as separatist captain hats) clipping through PUCS's helmet. +- Fixed lights shining through items characters are holding. +- Fixed ability to clip your turrets through enemy subs' hulls and launch projectiles inside the sub. +- Fixed "heal [H]" hint being visible when focusing on other characters while climbing ladders, and the health interface opening for one frame if you attempt to heal. +- Fixed inability to damage doors with melee weapons from outside the sub. +- Fixed riot shield rotating in a really strange way when swimming (in the wrong direction relative to the character's rotation). +- Fixed Electrical Discharge Coil behaving strangely (drawing huge amounts of power, but never discharging) when there's not enough power and power flowback through EDCs. +- Fixed "show hidden afflictions" button not scaling correctly when switching the resolution. +- Fixed the abandoned outpost you rescue Subra from never turning to a normal outpost. +- Fixed flashlight not being turned off when swapping the item to the belt slot, if there's something else in the slot (like a toolbelt). +- Fixed incorrect draw order of the diving suit's arms (the "shoulder pad" rendered behing the upper arm). +- Fixed "chem withdrawal" and "drunknodebuffs" (variant of drunkenness if you have a talent that nullifies the negative effects) not healing by itself, meaning it was impossible for them to fully heal except by using the medical clinic. +- Fixed captains spawning without tobacco in the pipe (literally unplayable). + +Modding: +- Added "AllowAsBiomeGate" property to location types to make it possible to prevent specific location types from being used as the biome gate locations. Previously this was hard-coded: any location with an outpost was allowed, excluding the vanilla "abandoned" location type. +- The equipment slot icons are now defined in the style xml, making them moddable. +- Fixed crashing when the particle a particle emitter is configured to emit can't be found. +- Fixed bots trying to sit in chairs someone is carrying (not possible in the vanilla game). +- Fixed crashing if a mod contains a wearable that alphaclips other wearables (e.g. diving helmet) and its texture can't be found. +- Fixed linked shuttles getting disconnected from ruins and beacon stations when moving them into place. +- Fixed shuttles attached to ruins, beacon stations and outposts having default crush depth. Now it's forced to at least the depth at the bottom of the level + 1000 m. +- Fabricators can now be made equippable without crashing the game. +- Added "TargetItemComponent" property to statuseffects. Can be used to restrict which components of an item the effect targets. +- Fixed inheriting ragdoll texture not working correctly if you have monster A which is a variant of monster B, and monster B is overridden by a mod (e.g. large crawler when the base crawler is overridden by a mod). +- Fixed StatusEffects that target the parent (= the container an item is inside) not being able to access the properties of the container's components, just the properties of the Item class. +- Fixed crashing if a bot tries to repair with a repair tool that doesn't need any kind of fuel. +- Fixed outpost generator being too eager to use modules meant for a different type of outpost (e.g. abandoned outpost modules in normal outposts) if it can't fit the correct kind of module in the current outpost layout. Now the generator instead tries to change the layout to accommodate for the correct kind of module. +- Fixed MatchOnEmpty and RequireEmpty requiring the whole inventory to be empty even if targeting a specific slot. +- Fixed contained items in contained items being positioned incorrectly when the "middle" container has been flipped from right to left (e.g. railgun shell whose tip is a separate contained item). +- Added IsFlipped property to limbs and characters. +- Conditionals that target limbs can check the character's SpeciesName/SpeciesGroup. +- Always and OnActive effects can be used on limbs. +- Fixed items that are set to display the state of a contained item on the inventory slot (like a gun showing how full the magazine is) always displaying the condition of the contained item, not how full it is. This wasn't a problem in the vanilla game because all vanilla magazines use the condition to simulate the amount of ammo left, but made it impossible to display the state of a magazine that actually contains the bullets as individual items. +- Throwable components can be used in conjunction with Projectile components to create throwable items that can hit and damage characters. +- Fixed bots detaching repairable items from walls when they're trying to repair. Doesn't affect any vanilla content because there's no detachable repairable items. +- Fixed projectiles not sticking to level walls in multiplayer. Does not affect any vanilla content, because there are no projectiles that stick to level walls. +- Fixed ParticleEmitter's DrawOnTop attribute being ignored, causing the DrawOnTop attribute of the particle prefab to always take priority. +- Fixed items deactivating themselves if they have OnContained/OnNotContained effects but nothing else that needs to update (i.e. OnContained/OnNotContained effects not working on items that don't do anything except executing those effects). +- Fixed "packet size exceeded" errors when there's a very large number of items in an inventory (previously only possible in mods, but with the larger stack sizes, also affected the vanilla game). +- Hidden files and directories (ones starting with a dot, e.g. source control folders) aren't uploaded when publishing a mod on the Workshop. +- Fixed penetration defined in RangedWeapon not doing anything. +- Improvements to decorative level object culling. Fixes level objects disappearing from the screen too eagerly when there's lots of objects visible. Rarely occurred in the vanilla game, but was a common problem with mods that significantly increase the amount of level objects. +- Motion sensors set to detect monsters ignore creatures in the "human" group (i.e. you can have friendly non-human characters that are technically monsters, but not have them trigger motion sensors set to only detect monsters). +- Fixed bots ignoring ItemContainer's ContainableRestrictions when reloading ammo (e.g. if you'd set only certain types of ammo to be contained in a loader, bots wouldn't care). +- Fixed only properties that are editable in the editor getting copied to cloned entities in the sub editor. E.g. if you'd edited something like a door's welding state in an item assembly, copypasting the door would make it unwelded. +- New actions for scripted events: CheckVisibilityAction, CountTargetsAction, WaitForItemUsedAction, WaitForItemFabricatedAction, OnRoundEndAction, EventLogAction. +- Made the tutorial objective list non-tutorial-specific and usable in multiplayer, and renamed TutorialSegmentAction as EventObjectiveAction. + +--------------------------------------------------------------------------------------------------------- +v1.0.21.0 +--------------------------------------------------------------------------------------------------------- + +Fixes: +- Fixed LOS effect sometimes "lagging behind" when the sub is moving fast. +- Fixed some minor visual issues (occasional jitter/flickering) on the LOS effect. +- Fixed some issues in the bot AI that we're causing a large performance hit particularly in situations when there's lots of bots in a sub with leaks. +- Fixed bots abandoning their orders (such as operating a turret) if the room is unsafe (e.g. flooded). +- Fixed an issue in character syncing that occasionally caused disconnects with the error message "Exception thrown while reading segment EntityPosition, tried to read too much data from segment". +- Fixed wires set to be hidden in-game (e.g. invisible circuits built outside the sub) being visible on the Electrician's Goggles. +- Fixed an issue with level resources that caused crashes with certain mods (e.g. ones that include subs with piezo crystals). +- Fixed NPCs waiting on some outpost modules never reaching their targets, causing peculiar behavior. +- Fixed waypoints sometimes not getting connected between outpost modules if there's a very short hallway between them. Addresses some cities missing connections between waypoints, causing AI to be unable to navigate through the modules. +- Fixed some UI layout issues (most noticeably, ultra-wide crew list) on certain resolutions like 3440x1440. +- Fixed campaign saves occasionally failing to load with the error "an item with the same key has already been added". Seemed to only occur when using certain mods. +- Fixed crashing when you e.g. use a pet from some mod in the campaign, disable the mod and reload the save. +- Waypoint adjustments to most submarines, outposts, wrecks, and beacons. Especially on ladders. Should take care of the remaining AI issues on ladders (the old subs in the saves don't get updated, but the fixes apply to new subs that you don't yet own. And ofc all the subs in a new game!) + --------------------------------------------------------------------------------------------------------- v1.0.20.1 --------------------------------------------------------------------------------------------------------- @@ -5,7 +292,7 @@ v1.0.20.1 Fixes: - Fixed hidden structures not colliding anymore. - Wiring debugger: Fixed tooltip rendering under the outer frame of the connection panel. -- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions- +- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions. - Fixed jailbreak_sootman event getting stuck at the 1st SpawnAction, preventing most of the event from working at all. --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index 3f843afaa..b7611ed1b 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -57,17 +57,26 @@ - + - + - + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/libsteam_api.dylib b/Barotrauma/BarotraumaShared/libsteam_api.dylib deleted file mode 100644 index 97b6446eef52ff3760bebf2b856e446cbf61616d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446352 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 z zfzIELY<9aGsEY|Sr59C-q%R+cpxqC|>LU+H+3*KZYock)gK=DWs;aW01rNBKPFs>U z-qqUSO0+mccffWVjgCSHjiwjZNmV6bptOtEX5}h25{si5KCz6K~ zR$5v;gjT>8YL>_YB7YJ)Vt^JrXe^c4)u~!%8eO$Dt*EMUlBQJaKy#~chMlWs$gsQB zRA%<8Zpb;BSREO+sybSd${5;T9l6-&A<0D=`w$nzWOSt5%Y9YVYj=2PR<+iVbQDp& zJWJE!ycP^!eyB!4az~&8Rc%h2+n!WbLyv%y8^x)r7B>cw)8O+@zt2-(CEKE7BG7KQ zY{ysYYH@Sl*nvB)6k8jNzGyyt4K3uX8831+B*H}3kS1LNZK{^xu_w`K=q)cqyyVv5 zp?%dtXtVg(C-O&;7sa=Su2*Z1ev3-s>5dRXX0zEiugi^qX>|1%hov`de+VrSLP&ys z@U^m@jY40eOOL8ncJP@F^vz3Lz~g-_ZHlzCb;g!9?Oj8C!PEw$(ret>w%DU-k*loY zF!dUZ@1-|C{Af(ur3nIt($a}q6b6qe=$PBMaHp__Tdi&sZvqu#_#m}wL`|+;Y~~Hx z{Y>jfpwKWH{W%2zHEu+6YD!&W=TORSk2@2mPrPo?z0&vpgg2VTUg&^QC%oo>n>3yM zTLks5#XX}P9W&*BJbbH4g;OQ$tF!l0JxRQ%`I@;BjHvIV7zvZgh$^#5^MW7 z!-g7n;1PO2?{lRi`Sb6-es_K?cHq2f<#O=+?kZl5w(wy+D#s9&BzNz0&cF0@6?Pvf z&j^b(#n#@|)8?Rok8-WGxy7XJH1yF97B?c=(qT`rC)#D<#Ed?c}7{wRrkxOnuHH(H<&zB!qsFN_JV~pX@8hH-oH@w`Xg)F>XMFJ9BGp z$@QBq)e52Siny-+BC>EU8^D-F5*mvla7$k><6&2N1*fkb4xwdG51|tA)DzP;OQOT3 zXVJlj%SuXRP&OjFS3H!?)wE9OUV9pvkR>hMVTlM!?7K;-k+(_V^ynW zH854@T6!D?_loyYkrzbX7CCqk2QfzEOh~HA4W#HFaW?w#G1Owczo3nEqA;9xxGdI4 zhs7C=meLayM+O8Q+FLi8PSzDi)tow^s&jW)R4>*>i|R)@=-5+jU3MpXj!d<9V_mK^ zy8Co{o293%oLj#JoiOY5Idw($x_U)B^&4K!GcBE%+M}YPQDNGK>3)jz*^8c`Dls(w zu~u~E@krWQCk}<`&?6q+6V1m>1@$}FtoDqwa*i|Wx3{=cY~|dF>T{dJcn?!h=?*-! z`>Bo@X&h-!S33#_16+$W#%*_Dg3?>vmeB1!!*!mQeQ5h*(&Rd#J8<&Y^YoJ#RxC&S~9QNBM$68N#lIaXFq0_E2aq5V?gCnOp&@S;9XNTy=sFT=c< z&(QsmF8c9_h~CjMeeL^XEDd@xMx}D@lN>qeU97bKNka!y`lP3|+l3{P^G~*NyVBxZ z8SW&T&&ct*zEl<loL;f(4|G1R_28c*A1b&5<#0oDzYclo=&r#G-kk; z@Rqxn^HR^-wNFZFOP)kKhBIa*i#4qc@`!JvN8vho3A-K>Ich26uOTT!PxOd7(NVsr zj;$l6CfC8lC=scq8FfPFx_B9|j6LQ6vvL?5)ZYLV^6a`69EvYNkXmEoN zx+UQbTFwz1Epn^K8zM`J*?q6bzAHHT9=cuEiMDLxdXEt;SJX{UQ!xKT!;`|Jf_mH{ zskni^-BHmq$YEGO_OE2QZKW0*@1mdUMT@m4yT##3p>g%~kqy_Acvp(eo61%>IHj=t z89gYV;t~&3Q`q zMhl(_p~AK7u2+SUr+h`Y9n3K>A(V%Oi4gimP99OLt>a}M{)o5w3PaF za3#WB7g@B4^-3Wrh%2g|x`GBW5T`|`YomF&hjxg}+01w=Bxb&-j&!_1Oh%#u!|>u~ zB!f{QI2gTaSc}FVZp9)q-EN?F^4#tFI{IuRc-j`W_lL-$uNnU&a@ zOH?V2zIis1<~EF`iw(o+`{!Enf}73CnT>TUQCsmMFAbf1me;~MN4K{)u`-XAb{Nh{ zF3h>(?0hrHZpd@9SS*rCwfLnW$|yZuZtFIh(hzH8n1$Hf%3)T+5L%CvpjvZ!g7pEk z&Dz;%hZU@d|I{$9Wo&dz46pw0c}`zxim-TLp?8d{7h2Gv=P;ZS3%{_1pcuN{FpcB2 z^f^>n8uMJwSAD~B9&G3x{MRq(ogTXL`OlRtN{oClgszE|IQu79CU`EkbwoJrKnVT6 z;W&;EIYs1rND7kmy^GK33p{k;1+*S^=jFrc&!@V28S*dM$x6_ ztuS~-1w;8`7(;_z#2|>5_P#*Q{tN{)Mv;AYG5%8IWRY`4t`fNo60>-lGX>*}oiFM+ zw-+W334KcB@2nK8Rz0JgU1I6Wm!h2!_LLZmxe$d8E~||uz7&bkwWYmmH=wpGMMV32 z%VB>Za-ztuM6M9|wa8NH0$&W6&zlH+nhU7>!DX3&;OOC;x?8ie%1hNoP45!y2insrZ-M z;@(CfG{VDS!vb$$LuzlUPc*UM9DrF$b(ua1%e<@RSNcS*RDW1gi_shIwaLuiRagXarq zmJUVy9E$s{tQ#wbUqeG^BQ!&3x5(on&x^b%GG{MG+;^AV+E@>X&Nr5%4Q;|VJ9N3| zkG>A$K0{TEBVnCnm~ahgUjY*sXNo)^a=<=zSuS$GegqKg3t(Uqu{%Aiy*&+U*r(uq zITUbL-><3i8y@CU9utPUOnX|>#A%iMmste1$u_`U>e#*}cIQVnout06P-QWm8>SL5 z#~9;%Ve#0Dr*mOkEqU%rmsJ|>AQE34kVuG}dXVuQkt+{ztq`-TRGfc+`oP1ivR!1} z5yodkPB_Z=cadwqV|+{G{9}wSid^(P&GZMNOpke$ zXHD+1woN6k8ci3!TG8~{S24B{qwB>e8ox}EFt(N^apG0|r-#l!bC>8;v@z_piV4a` zkMx~b{Y8ABGgM{d zJQ(*z`|ys2-vw{9qM2`~S(?H9*c+JlCSlI(avNMvyn!tuiT*B*evrOxU{KiohUyVL z2FHW&bxeGn6?t9cfFC(C@*z=&vA4);+SzaF-Q?ysF;*7l?F@`rs6eG%|CRMt2rV$M zD-=sR;BBp#-6`^@$TOmfY}BJf7v7YL7pg2g&PUya6`pEm=Mv zzDT3{!s+H)aWt(d7AYNer{4WvZ;Haa+Z3ym+!fKXX1v0RK~%EcohF?V4`HZpbG?LB zaj6Q4H3acAs5u2$#eW<+ODr~6uu|)xiA|G|Z0WMwOTRV6O0-1FMr)d8B%-I%jiCL= zm;ol(tIy<~re50KjDmReLBCO)ZW`xuxad3#9g|c}{)y8)SL7;4bZjVCtPc4XW_@|J zpqbRU3bDS^40BNNr^lM`S(cH_F*`lUwm4y&6)G*4oBh@7H5eENWlZKoE)!WIa<9lz zk%gxua#(BE2Mg<(V*##g9bO;*Uz((0WwK0@PFB#0z{=^>Nt(V%H{;*j7n&4FxtnuolPe^bm!l``)x(ll`Sw}McnT=v0Qv$(=ZJ}7k+(#y{h3Yg6nPYq z*L-AiMxR=(3+F1K=l{?G)7zOu6C(Ag0xg8|s56?T zJ5P-aq3LHhGZw+kS3PJkoY#x{BTKKiHI;*bYIL+*K# zu{7eLuc9jE1vW4WTk&|boL8O+q8664!@3xU63it?JE9~5B2zP*AJLwuNIHbb5BP-vKC{yR9N|E)H`} zl+C`@{4^MqE&imC83Pq@`0x3fkt;&7-{x|LUW54Lf70%!KPcDT`~m2^7SN~*{= ztx;FcY8plespj^r{D61`ogJPr{OIgw7s+BK^RMbwD|2NTC4(VyP^^!R4i;kxw z(dY$Hk+FM&+IJzki{0bf$hs1Z{ZNd8(V-Z13Xp=slFVNu3U)6l)|qGvwoPwFhtN%- zhMebYnjmtn$hD9Z#NqmKys)h=f>cmWN8)#?g7? z$7S)C{VRv!t1gmf9?EaWeT3Cm9@yeVSR5Ai-nt$b7w*N;*i-|>4SPnD)j?bvj@uq3l=<_IPQTeu&dUmf)@-?ic_!X-tPZw!-cimPI>a zpHuSo8pnz|=4%~!_fL=a>KJ_|gNAi77$&@*TiGe-q}!R<2}@g-xV%O{V#LN7-!p}G zF(u?fop`g(5cfFjqhg?s?3g0n=3ml6!YMx{ngUkR7j=|F9 zy|VBWa?Ox>4^4X;>(8B%RcWh{A1y#&lg0F0k*g#CN&U{Z4gT@AGhgly=Ym(xQv zEVam&)x4}&VLPqHjE7}cjX<-R@Ao}24hGf=J+CyadPidHJIjf z4X2%5TUy*Wj)qwYc1Tlkk{3zLly-HeYZT4u<_{psmX6gin)Z&q7)r<9G2WBDd{>{} z_?Pv!bdA6mK&szp#O$nOv~Q@-Ga;X*ht73HlxIWfx>Un>T8iaaXvjCf2>hng$7 zQ?f4ucyH^=cW@ByFU}qsg7B`2=YBUhmcvCZhU6M6B@<?KHM@iHBx8}DEUorZ&{?JFICP0Pyw5*hrvp>8&c4l`CRFm37R_6KY9kVdRn<8_T2zA8@D1DHb4{?*|Gjc{ zzMBI60`pjMF7k8q;}k8DX3bH7(Dtve8W6`tbm%=)`kU|Jem$#uIBoA9Nqyeuru6mu zYTigce1OqN0pKq^Qqbwz5a$q(3>6mHv$n${GC= zx+lP+o6ib@Tn35TkFSizC(sJS={IrJ4@*edEyjs@xM%fYKf^>$faFe^2PJ$!S{-oH zS9zg3N3@DXZV|a()YL^mHuA86?k8_4}6eC3HRw>>2Vqq`*EQD2&6!NG_X;3t8{ z5nGYlMOQ|2doAUL-E@h@*mgKfpY>T0_Hi?WLvty~gJL?IAw^c&w*qVp739p7_Vvus2dlrtduD`^N zW@<^~A18reJ^G&%iSj$osF7*_4MiC1deR;@E!x z$8@I1uSK2_IcOl_Ua3NX=cM?98EW&BO-6I+2F2t zKue$O(zcXn!}i&?DN%N-VseooIXP@#6eI=9fqi3=lA7Tgg&WBO9WSs8Bfe_!C#|4a5HZyWD}K^Im$;rAJN~A=%c3YlWZ`D}Gpi-)6b?SxDyLd^ASpr6v%5@kVOZt_>s^wiu;!wDjZ#a51mPUbl! z$Wc!87{Z+t`I~4Mi>CyKr_1L?4ss(am(w;35?dm@IMXHU+fF&DYx8tbM|mUpY`B~t zu~7m1O%Q)`L~a#%P~=IGzlqEm%C_f-EQUnU&`wv3?>xOX0rz2a!Tr|YFcMC%=(n|B z(9Y$l0=-oX9EnD-MLe?#^-K5BG-4#imGT_Yw)8mqHr@X;)ufO4-Z%YY)oDxT?Q(KZ z(>+nxnUJpB7EWtD@oxGf1LsHBesC(?PWQUf!|^bnD~0{KoE==ec7!#7UotR&Xc-lW zO(8dZ(-Z3i`ty5ZK6dqrz{(M7L5u~1Jo=$0PT@(4vxmWGl}NhzaX4*tN6}SJYr4s2 zYoLqiGHuT2fI~Shz4$I>$Was?`+!rcpQn13olaE4%IGK>*$b4pNu-!pfeIW7z&D9DEczYq>()>P7gLjRPm@F1|iv9c*H6z z&tLa!W5F79QcQ*eD+F^7(f-p{q^-Ch}ip>3zLmelwP~Q7La#BCv@glcGmXFmCB8@vD?>%_-5BEMsQ zzP2^$Kl<(NKfbm#^*>lW7QJgC`{x_9OLXn+e(l}R9svz3`HDQj+Wxk6=Rc-x>v!aL zxpp)DQ$ySO6PbEROqJ7FXVkd?ox2if?ntTYkT_w(Q#iW(X`r?>f_5bH-o{w(qjk$p#Ts9!>2^P@t?{Oe~GyR$D*l#|8BT#>6pZWDP(Dy6AC= z(@!vcfpt%PMT)Zi%R5XoaUfhe)2RPqy91jbzQN{(|6+(^a~oV%{TKU$`j($h>4*Q~ zCZ`TPOmbx4up7rcub*BrgHiE(x!apo4=tD7y=g0F zHtXN&TQ2Ee4nzorV}msPsQzFs_+TGYLlt%PqHhOia%zB= zrg@T%zdW(HF+l6UkLHFudvjQS4#4BMd^AX=q5TKyF+D%9d>xvdi>TRf6xc}<`)d|H z!Whvz9_49`vh>DMFd9ddPEJSf=qL}39bk;Thb9lu+Pj=7#_iu3h=Z?ke1&NGD#t>^ z{jY_%d;oUhF#_TMXw3kPr=?!ZB6zh1Q~m+Sj=ho{{l{^p6pK73a?*Hq*(0*w1jfrn zZib|wNDH4;@J(jucSO{dVoB=VIi0uas6UtNKGFYCfhyPx=(*m z|Cm#jjX&pThHdv>b8repHel#Rj;~qLpE-zuY_%WA(Ri=@_Z$S*e-fu>l*m~ki$!h| zc~s;%k+($-os8%Ohg0Doy{;S_WUSR2Ih76vr_<1U@zCBN&Cs{tur;T2lb9-}*Ke>t zx>JV8!)Lz3%^q5n3qSiS`k|#|URDi8RVnkZls%kc4{~ULb`3@*;SB_|e=vxvATVMm zU|LR1aHM_RPxcV4?8pcc^fk8uZcH?Ch%z&N2+WLufe@N4a-|qcr1e8I4|k)!S+R#k za=zdwhM@xd2qiiGBjz`7Tz9bl5WSfFJ2unMRm(d0kz7q}tmtdVxthv~T`EisBZo6n^O=G`b zid-ae)3hMo8AS)gx>%HD<^4jP5_whR!0GI9jL6v{SBl&LNqm6Lzte^r63$)>^QBv! zt@hBkVcd060k0RG7oRtDpXlajlV6*+pSBIteEUw{viiUo9P~($(?l+Wq=#f$-8WB* zqd|Gt%$c3XqlSHX=(;RczMuV;r?sZ>!?kcaG#s5f-p=8*wYBuL=^i z;mqsBe64Mq{z#s`f4Y^A`0wS|2K(L{GA&QT5a)WHhM5B0`4W67^VlWCL?goO>V<&- zeUpdo2#?>Dg*=umT@X#ahxruG=V{XMjT{d8HfS8mo(XeRWKrhYms%T_+x`hJjXALF zkB3vKPrior65PuDE(7y9{jFR{SQsCgk6uU?#13%=q+s{L;=scS{4_RwpRYxrXH3D9 zsZOgrVDeMG78MiCI=o<@YyHA89mA2pn<&sHe=!_+hK;oy>^>9jc&!j%;(DG=`2$VTS06Ru#T~lpX3fVIPsyD?X-gLBXgvQ(T$aMt2nW>yv9N$qQ zr;A(!Nx|HT>EZ}<@AStA+zs)3^ext3FFH3w4lI;?3QtH@zR2<`sXba5ow z%f``sucBW^qX8Wn!*_9XUuiDsF?NmR(NHR%0n>YhQ)4wubGxp!?p9fANW-4 znsG8-ABTd&qo<=Zx5er8+rq1H#)9zDq){4HXHm&@iyPsJ;g^gLrtFNCCE53Lx3BJkxSZ5oHTAPsZ{NeIGOizwPN zMl&Ss!gyF%Cw^`8=NL^6qOTl}I*YrM{|>~7;fX-W1Y|_{C@dSRIjtP&D5Mz;$n8x; zuR~vr)406!To^C`fqWy;{7&TPc^u^#A{WmK(r_e%Q?fxEz7_dBB*szHcbq2EQ(q9Y zkn4oggXMm9dOHIZ18eFuW1MDH)nNHJ*g7XxZipN>pMx7I@-8F>U?}UuoHr_l$JtBn z0#+Lb={wo>=QtVN`_;0L{-AqQArIS~H(t+k51ru5EflLq#H#N=1sxl&MsdnCXYNhb zp1hFbFjwR%k=sNb5_wAG?~pi1D=Vs0G(n5Ny@D1^z}=D0{S;5o^uttZIoJCvVtd0y z{vh&_$Uck39TKNhaPv3^?;hL%ELOa`j^A8E11BP_GEU+fU!RFurdz9nA1DbHuUU7~kPsRC`V^GY8^|ZMq=u&%OL0 zI=_kzDvk;jw>M-CdL`bM;_}!w1rtgqW>x%pFrEaJf6V}W%eHQbtwGB<9cx4$hvdzJ zO;fe-Bz~?1^B3O>fF@6eN!l}AUQJjuLu*BQW}*0#YFdS zvQR6tbh{8kA*f;-Uq4}sZCid0sGR@4bCBcFcyGuwjgQP^O-H)_GX7TG^r_5XBjdkhvPbo zO`|A}&eYz{z~HAWLh4SsmkWmv#mmNxDrLnTE-V*i3?$rq_~1B78H-ZV;h!zk%9pnC zdex*Xpbsk;_Eh5;Hh_$$@#eTZ%aU68V| zmOU;Nj~hkqX7Bg$5*;7S_cgo~Mbe8&X8I2e$g!JsMbhnUF49`!_Kf$W*kX_r9)Z%% zxfpq8&%@LTuXkWq+Ug7|?>#ScbEQUAF;LjM7_~X26RljTMbPP`(tPGG({T0#!xY2R9%Hd-%h7_S z6l*Q<-kTM&8?M!m<+e6eL-t8-S?383H zn-iT_i3bHet|WfBAy2{Nk?WVzJiy?X83z8vj*_j3QTJB?c58yy#}(EQOr+4B>uc=a2H>~>kXRL?Kb@Yerc zxHDox;zbQYu3TG=yw2Xt#Suk)mTK51@ZkOiPln~_F-tYx+nKrox_P4O!g2#r0e70s z7AC{)U$J$sp& zHE~AC@Z;W7cuX0qbh@tE)FjOgy?ku^ zXsu+W#wQBVVpH%4u&+pWOIhh7>50oRKtL~q-Ovnp=h5ccl?XWxVIt8Sa!@7@e0^_d zDp{_j%TVbu=Sq>NtrRt}Bh)$QAoF=_LU&kgtEe3mc~X?+y*(p~ktH0u_!?gf&Frr^ z+&oC)mq_#Ct8Yae@2+2=$%wbJ(^57-#`BrTM)VWD(Z=x=*wZQh*36kJHU0HM9c{gQ ziP7Kfp}(Q>0w;Nr1YIO@rO2%!4?_9}A>sNPI@RDDFF9I)+lwbj51meOp`8?8zlqG+ z%E?(M@|wv0+t^yU*L)VN)c8!VziQ)XBgbbeG%-mMIdD7U5|N{JFrFo{SmaTW=S1EX zIdLcJ%oDjzu^g6R=756LBZV#u~tK){B12#BK3a^jOrssbpz?g{D=24E> ztMog&3}BbdM{$&f$A)5K3_G2L5?ac4EDwpCaE$RTk?X%_JnT5joshl+UE072y1GH? z#*~dxQ!Z>QOUcZVaw#b)K}a{)KgKM$Y2h;%<2M>&a(I(JH3h6X`Uj|1@~w89*r@UO z7@oQJ!V7*eomjaE6^W}3ZP{cnvv(6Bf|X<(MmW9+W(t1@(y){^ZxdDne8#43s%UJz z-`LJF|2NqGHr@Xc#y=rSXrrYpCyD%7lCmS<06s2id=Y#T`oeRhDwzxE#XX$aq)K)PM2^wVAIJr|2oeW z?~0!Zr`hKWk+}yd;d=|Udi2<uTKwZ_d@D} zrz%%}vF57#UoX~%Rr*t#7W8Rf?bWvTx4r+fZi&&^T4-p2wk9a5;}orX^X3b*quNd_ zbC^~bG*cU(rPr)EFF0ww_QHJ)p6yVr`n~>fxHW6mhf??I|7OMaaYcq!f3tc-_3F?1 zd@^6XdT3~9vo}LS#jzRxY*#(>%}&jrTRn{bMdE4(Yt6!%g|(`KQ>ycrx zv4~SO{Kx@eExZrlbk2)s{YyWH?)=dQL}?D8En^3!YXOLI#=eMJ+@T7<&TOP z*Qff`{PjSn*6qUEw2!FX4nH`09BHFM8Ag22aGJ%|toamjmFs`~MyMdI+Qc<$7TWM{ zkJYVOte&rp)SA>CsZG(so~!#z)N?HxyE`_2sZP!MkJW!Hx>l1~%`;ooYW_rc zRP%@G+}{~Z?&X&u%>Q-aZ=_FMKvW0M=y@L@+O6<@;QJQp?qQD-wNkipZK8n+mjJI- z_=$fIoltl;@ZT2ce%+4|wNv+v}{Tb8~g}VVif$x;FJN*;iS68?Z z@I-}s0Kcq z_ctSoS9nwlBDcaT!in;MIX(wk6RlF%8jW_L@KoUYOY}6B0zap4UR$Cr3V+xR<*4wh z?TN-JJP&xY!c9A%ycD)}BnsQC+kBuCkzL_D;Qk7Kg7&dU;je&CD9qoHuf0Y0%ioc2 zt1y2@et^RK9r+T4`8)D8zSi~mJM!%n=I_X7E6m@KFHxAkBY#(6{*L_1TXnzu9r;v+ z`8)E{73S~ApH-N@Bj0eFuFv0*e^+7tj{F#f`8)Dk73S~A2W{8&`8)Dq3iEg5(-r3L z$WK(5zazh2Vg8Q%13PrT{2lo=3iEg5vlZs=$Zt`YzaxKLVg8Q%(>ry)je$Q>n7<=G zOW}Or%L?;%+^TyhbYY7k^fp@{*HXLU8qZy5J&i{tMF~$#tJ_H-IfZs0q&-7 zD)2`NX90h%Z~<_h!aISdDO?4Am*J{Y8GRuB-c+~;{=W9D?p^>Kr|@#%RE2i~7b#o= ze}7?Il_n|K?GypG zQ@9wozrrQJs}$Z1d|lyVz%T9B{hk3%Q1}Y)XoV?Bqa6xY1-_whZQ#ZSbiWONQxt9l zJX7JOz{eDB16=i>t{)E^p>Pl2bcG$jvlY$+KB90x;Od8Tzj?rKDLet#qi`Yc4272f zf2Z(z;D-+Des=%!g%1OFR`?k3hYCj}Ycx{fHo%(|ZV!A* z;TYiR-|IHxf$J;W4fqv>y90Lwu2UUj3g8rlt-$>hP6eK#umgC#!s);V6!rrDu5c!B zt>e1QPl2EF;SV+Xx58P#i3;}v&Qv%DID~K(6!*uB=P|}pcmVKI3daGzsjw9|R^e`; z=))DBHk@db!Xtq(d=Q%hflnyh8~8zl!(lmqUsJgKr$h+~e+%4C;a&eBnyGLtxbIMS zJ>2=VRcvQ5@ZW-TTp2pAEBx=z5Qf5y;66#=W6)o(@O{w1LW0=Y0G<1Tb^JZ>^9o;w z{yPfygL@x^&%%AK!p-5nOW_;9Hx+&g_yuGx$Mb1~)k)#SaL1#Q;vNe3DGHy1`*wxj z1wO0raNxh;p2Kz?hJH(hd$JvcQ-Q}SydsWhqryML6TUd8`~4d3bt>!l0B}o%i+~*p zZ-V{^g&*yV@sh%y!M#-BTfh(Ar`vfPxQW6`fj?0APuLu!unX=h6pjNvt8h5%KT<`v z^DErnQaBs#7KJ+j4^j9v=qy*bCEHQ>8R*owU$@f)xT(TJpp&BT$8aB`@Ce}b3Qq?< zr|?+l|KkDO&Me@T3gg3oWLNky(ltzB3v`MVeh&D!!ablL@}O?#C+I(`a5d<}D7**w zGlfSEBATP{Lf~TxuLG`udn1?oH^6Na?m7hZP~m}apRe$Dz&|Nme=zC{`daq;ci>2c z6M-`n?hXAyg?qwgsls)EYomW+ztf=8TH$NJy%hch7+?Dnol3BIOyQT{x7I_tdpg`( zD;y2?OoeL#&sKOGboMIzIdJ6~y5F~;^SZ(@2&;0GVk?d-K;46pF# z?-0GOa3*vHDI5j7RN-{!98)+J>8Z593oWzkdN<_K$y z!Y2^_4GKSv`tysz8Ng3Iq1(wuxE&O32kch3GVmyc*L27FgTh59qtgnP0@rv_w^Ib0 z5ehd)S-2Fwf_P3+cpl=jTj7Op|5M={)Wa9*=ys~Uk2?ne}^3LI2Vw-W{2Sm9TJlN8+5zN1CCd?2XMB+X9{sYQn=qN+>aD)4*d92y5H+F(Qhcc75FoS zzk=Td3fBPsQQ__I%j-wn_dYY5C|u!g(CMY{zhP&(!dIrE|4}$-8rGl`4xLW)Xan6& zf8a=kZveX#o&@~~3VUGlfWp7Qz1B0j-`g|LP8Hq?zaJ|6GW7Em?gjS}g-6Xm|D*6Z z;QIg6?GyvIQFs<`FNJF$tceOAh5I)OpGLYaD!d)I#{s|rxN8k{zeVu-FNL?ly^F$maPOz^O1OWe@RKu9KNT*5d(d;boi4x)6@C%8v%-^L z=M#mkaG#@aHr#hBd=Tz`DBK3P&hxs>Y~Z#E=K+7J@O?;Qk-~N0{++@fj6k|x(Ea9& zM7k7C&c~Xn!WV{Ptw~`A^cO394Em=P&W8SdFY0!70>7eg1oXQp90UBh!h2zJvcijC zXQ#sV!S5x7*F(S7OS;V)z^^L&0PwpCj{+X3a1QWXg{K4WQ}`3$YYHy{uJf{Pvlqhc zps*L=eyVU7bS5i&8}Zzxa8u}CQ}`(KpZu3@CzAg{uuB+N1C=xZhSd3;4O$bvqM)yDD4)JA)Oz54c$2Qs9#c7XsIKL-!jB z9Hwv`gk@3qVYufhycc+l!apP54l5jlaIYzx4xN9zsoNX}+(O|i&`(hKX}Awl_%`rL zg+B-WN#R`JT5sug5`o`RxXEauM1{kF2P>RB3in5aJHh>Xg};OQ{Y`Z{(||)2ejhsT zDEugJAB7h}XO_b6j6r-9t_u9OX1blnfLkei2slmQb?}?7@HV(_P9r+#zY zPE+8v3jYh(t8h2q|HIx}z(;j$Z=gvC!JXm~oT4)k2u>6NNeD5VVUkReAu?fRVo)ql ziWMy_PJrT+;!>=*yA*e~V#Vcs`|N#Y&di*dIidG|?|bh@e|Iil_E~G~wcXaUzuyZQr!@iWhByb?ZuR$}E;RV3k8U6)0j~}HU4eZVEO7NyK?Ai}~FT)oQ{ujgZ zNL>Du_b_k^hF<}z8FmKmbcVMf{2;^kfZsFx6L7--DyJ`SBEvm_7cra(e3M~sVApn( zJ_NWc!y|xyX1FBc-NW$c1k@{r^Q*B&Xis?`Av~SoE5Ls+d>r^K!#jb!0xA7F;4FsM z0!_`aTSAfDYprjs#xI@Ce|G3{Qp51%s)a zdr8gUo1Nxewl(*e5%rO|w3!KUDEzm4t zcof1cV6x{()-3+OuNZC$O!mb{_<5uyjNzrg z!x-KOyn^9(z<)7(Z7Aki3_k`gA4%oB2PXT5L}n-GrewG*@IZzO08eGu9e4x7wSX@$ z><#>m;YG7CM~$K~rvs-kybgFX!#jbUyHJ`YGf_Vo_5)TkJOp?a!-auQGF%t_+JqfIgn#Ccx7fUIToD;fKJ5yHolb;BC+F zF9^?OI1_RG!SE;0JYjevFxmScc{>xhJHr*gJC$L7@SbD125^O*l>Qsw?hNMxp2qM1 z@a|){F0ex{N?#bb1;e9&QyG3b6ZL`NV+eo9a6Dwz>P>ksBV57oUeHft_%!f-hDXgt zzsm4Zgf~=B-W}kLV|X0GXEXc^^hX&^orALOL+K-c+cNA0Je=VG;GGPgo{K)8;Tym; zm6UfZXu2@`6yakSu7~hH8D0>s3>pKd8q#k ze}bIF42L895ySq#4dW>Nr-5j*3|||Bc`U)T$5UQM;3$SW1CL?2 z@DS8zhHoJJCc_&MUJ~O6sn4f@TQfW#@{<_ei|{!NF9qJu@HpTX3~PaFW2_=_Zb1Ji zhMxitW_TuO7BM^o_&mc+fb(K}BD~{4--zMM)6o+)li`DDC`S#Y?}PAEhDTPOjtrLp)-vn{Je%RRz$Y2*3;d4Z0>IUgk3?oo;7$y$1RlolJmB>V zj{v^M@K3<5$X~*ngS3Y-yaISA!_L6V87>KYnc)lI&4(Z1y@!9BFx&}g$J=#W_+H?J z47-B&2*a=N?+1p5BfKttL{2*X?Zt3GT<#1-y^p;zLos46g!i+@JC`0Pex?eBdz*cK}|) z@D$){40i=~9zc1k0J|}q2e>!G<))(#XZU;I`3!pi?`OCWFgcSZc~u+uo9`%|4D88p z25>Ki^G(8hfZ^K{(H0oqJr-%?!^a~(8GZ#!&Yp?eWPR{`|Dgytsb>oR<64DyZPv%q5+eh$2q;RC=A8Ey{!3!<+laymd}eTKh9 zco@UY5Z<5Ry1;W8E(^Sq;hxa{5ySrgmqC9`WR8W-0Sre2XE5vqJcr>yzy}$w2mGAj z2;e$Hshm{cD26Km4`H|{@IrK-iqOq$p09I9Uy-U!{G>D&+w1ny~?l>;RT0NIf)2w$Z#3p z2!_3YGa3F4cm~7Wfj2Xp7x)sxb%0&Ir!oV98!+sTxPlng0Baat2|ShI6F;KS;2R9j19tv_%1K7P)n)i4!h;xIjXY0h zco*;-hED^ZVz?uC-!Oa%xWY&(b0TQkFnkN)F$|Xj{*mEtfj2UYcZMDAGTagUR>@IR z&LD)hV|Xm^aE85rS1?=;_!`5F!CPQ7Ch!<*r52+v@681PDl(||8C{2OpVv_sNH zrbDI=!>u7FmEk_X3mEPMe4gQkz%Iyl!kY@-<_t#wCo>!XOzJwJi3UEx@O@wxlphIy z2kgu69O#qE@IHk9%J6H91BV#C5B!|rO!V1RQGP_uHsEdyy8#bp_z`6O&hSLw+YDcV zoKmQxg!dfc>dbH@q-zkvn?b*X;R2vJ!Eh^tzhk%*a5a=Gk&_p=J;U+9q@I!RYQWPN zUJN-08J2g&J_ExwfQz9H5&9> zpF-$c0e4_{DR2tI-GHYud@>6AdkmNBhWg3yP{dVoGLe14svEQyc>8Q!|TEOln)0kH-*aS0qoDP z8aSEZ`;h+&!wusxUNGDO_#wlN30Q0YOy%5-Lw#oWA7CZJaNOcBgyFfszcSnd_!z^# z1Akyx1^Eqsp)$JwD;UlL{5``NpkK^zE9meS!;=yIj^XctD^I0z+5!hL91J{!VKwk# zhINpCg5ejyuNXcLnyS;NoT>;9VE8t0KZa)k&tZ5d@P39j0l#B-JaB{QRL&vDR5H8| z;Xg540{Bmc1A)o9h}6Snz$Ir;d=&DUGJFZR55sGKvl;#qcoD<%-7%IiT&gMNs0>#F zzRmDlIp&-UAM!&#Ka<8)+8=WXhSdlk!0=DLXqya|K=@{c2Q9%`iD7ThxXhw*p7@|W zGrSQvfZ^WIA&y~Bgb!r+4dR{3@IK&m4EHI6bu7cbv_V^AxFgzJp4l|6egm+^W4JN; z`xXp;T8+3EuCxYu&TuW@eGJ#`hB+C-jrw7KU=Ecz2{LOjTnS@rdxndm&XTn`N48++h!;2Q8zhZbX83%GGeO1%}cZQYokk1USLRz#8zsDS548u{#+vN=J9f7?v zh99FX-eTA_6=wuK&JBo%DW0QZVaD9cr3%2z`rm& z05W$myc762!;2xaD$14Q$xYx8hO2-ko8e^0|Bd0lfzL6#74_s3!^ctAsxP8)f>CF@ z8J>%FAIC6R6K6C04S44=d`}167``zCdNBL|ZT}s^KcK#qTukL}&cHg9;dPPNTVwc> z3iB?8CxLe$!()802gzbXz#{~Bq5$u9##{=&1Na@r?+AWJ@jHg!ar}1Rw-diz_>p@9hwwXv z-&y?r!tXqO7x24?-zEGm<97wWtN2~R?>c@r@VklME&OidcL%?__}#08T`(%zsWcL_qSQt{~z-NX?0NGm5TP<;~SysRF&XQ?zJ5S_r*Ij(45RN+>)L? z=|P=3~dT)c!;)Q$>TAjiN zi7@lEElvkdl|t{?)`*OrxUEr`_~f=mBy{g>jmGGjMy!O8G&s^TxC|$+4%-(*-rf%| zahNTUN8oYW8i&y@xhRF*%A{lU&2~-Vsi;3~t1*c?7Pm&ue482^l-r>bdMg*tbXw`? z!*te!p`P%bEAZ-EJof;oHClgbKH04?5EmW1lOAg{hL#3@Jj9c`H9G3wUFeP6HYN9j z6?I}RO2`**kgs@@wmJ9lH+RiQw?onq>9}ScHZ? zm1)+Oo5-d)ls-0RR}1>e7WW2aoH7Q(x_CV?WLNtLgWq2X zwD9U{Pax;UPy!Wmw`!M?^P-ik_H-~l3vX8kN*azg4dFS)E;Ss|;5A`gf>>{E-Xe%YJ9~=S>PQTA zln_a){*iihlKoty&Z(@Z#(G*?ilMl;UNA6}IaIWkOb~WaYSF*Pvj?c#!%vSTv7s|k z5XX|H&Py${v_Tw19z(Sy-p}Zym_DA3gye$9(u=1J?NE9`H<@NmM5Qny_vpjFwvxo6 zvGiqXCkBB>RPLc$>EzOM63FMBnA(nl4aBBSmhG~zQH<3?gP&rle0V^aj9UiZ&35?A z-O$=5H}QTeA(Kt$CFQaShqP=KCKsubgaBHbX#NzX8xCL(lU?gwrH$5fVRN{ zUy>q@vTZVOZkB|ue(h6>xdOJ&Wpov6PRk~5Va>KHgOQ|aK$*SbR7 zD$F-YnF7Ca`Yv!fqR9wM;pXO6)4;FY2^TjpfEzAutzz@$c)~h@HkO zp{yw(RhINUwp10;a(-CsDqufB+*+O^BlA(_Hq@?MBdz2llEiWE`CD$h&~$OTj1=j+oKQAXs!3~s6Qq=WUVEkXo!a!;+H{}026n+%1i1c4Vnn4 z;buW{zMJt$tPxy497J5h+E0g$^GzaAFeMK3ldL~wpdGRjn z!QYQnvJUVh!>2w>qe-?(7^G6B!(lMZC!3TcTX;fglQzuQli-VymZ!a*ogEgC7}$it zX7gp_RJO3Evzk)IL9;Ioo+hay9cv{mODiw8zmQFYLwQr-~u|SnZMdr*O(xq+Xx^Be8NuAs{3!NU$#wr`9HO zp`6QN^C}~t&vlx@ROk~i+OjP#Od+ftBno1odb1o0*Cc6drPYL(REr=@bhPCJ$V9xr z6{;k4xC%n@%6{475FOs0X`y38NW)b)@%$ubLflTbf?HXVb}RH&WRD5cr!Qn7ky5Y5 z65}8Y)NhhK20q!-JhlHgH%d|kGUH-8)9i!CvaA3iLd^yx`mz|KZ#7$$1MQmRrm25D81ZjhwIH-g>L^~9g(_}%#Cn8deZWL{Z z5_EJz|CKg4$b5_R5o{ndci|_2Q{dvyCbizom!)lDb6=RYsYzzp>psxma%3u9j8>h> zd2_Tclyit_o6OKAIg1D4b<;LI-_6(|3szEBlD35fD@`0w+99TXJ8ct%S~v-{L(klc z+NKIa-;I}vY!lJOD!L^gXbmVwO!cV;P0qKU9icp}H0h)*Sa^oD5rdeoS#xrMfr|oO z*id0&MGnbLsL0ki`c18#wP=f+6H;R{6YD+LKBPu#lv>N}J`-AQ81m6-GLTT4QvEaG zkyWqipiEcN7Gsl|*y06glyD5IOwz<-hM9_8Im?BzP?Z22LD-auaUn>ZlIRUjb~?3a zgB6Nvr}0t7SeiDNl3>8gu$M)m$B05P9l~>wN;G$DGuh5+(2$cBo0$QcNXtD=p;{#S zBg2AhN(t7a=~RB2G`LhneX)~WjM$E1996I;-F9JfRCW@;(R4{riR^(2(@Azj;LlK% zq)8>4Q5q3*8zDJ5G#o}@uU%RL!Im=e(Pk!+GsJY2ls1IEid4#YEslUXMhDa%jGCNb-slmaz-Hrw;t5ni@Y)Xn!t1_%?|Fw^Z z2vtzqR9b+`|eYBsBa{EpKKW8e_TGmC4R#Ab=kv`l3iiLDg6rDQrR=8`;0nUp40r;OrZLjrH!Xe#`e zWIF}y3;;FNZpJe+JxL|bXzj`*)A8IAVa`qy$)`}fAZ_(7$S!;qMC=`{(1>R53~9@HMX1s&we|)NyLq-I2}@XZ&JkpCq5n3W?j>iN_6sz?3-4y-3cK%V1_h zt*(%n7J6Bs3lg=;q{NbqI4#KS+iy}9lrza_RvIWV`NDaNJ%djpxjJ5r zsRu1j+qq|AO=&OtOo;8~A6Y^2nSeYYeU+8#7Y4<7>q%ivw9Q$C!&*Eg3 zlYAqcZ-Oc&5wi-M7IF3|qP2>T8!bu)(?2^=`@u+2i#-BNu&7{>ddf=#E!t?7T{{2F zm;_}?JQuaCZb$aU39l%0CHR8#vOHc(13+0*Hj$(CC!0%90m>kVaYO98T|G%aXb)O24#LfNvln~mzH zvt9p~0J|kamLr`0sBeaSZW}EYd#n=MHhO$Mp+4Bcu_@C)DSkX`EEs`MY{ob7kS3K5 zaJFd4MrXwZ1D;cK~pVw;iS@Z9vK{TJ~v6ZbFT<&KV1_?nbUrGvafz9>?|DQfPxDK*zI*{WNlpXLjL ztiA2gvJ&8|QpxUI)8Qvow+Osa4q@@4GkFR^;#`lc@vz?T2^GoOE(L(IxiSV(>yaq(xULxel4p+U<_X$p)u|wVM%Uig2{##(+O|jx1+XV$@R7XUK@$s6hlvPoU9w z!e-s&^TYOCJh60A4#}ydAWg;_xM=U|DUq8xTf zNtW8xmdGT85^E<=ubEI|V9HLb7lpMy57XY$U~H>U1@1Wh9a(L4PT< zm@tbRR|}G?peo=At++TWtZhb5l0+hwR+>Du#8Tljl_r@Zl_-|j0p_}>F^8e|Ye;HI z1s2;@5oRLCz^NsL8f}zSTRxAhjPQa=W#lnLBQhm0Wx!N4mDEDf$*?JDXh^%j7)^?z ziASc$E|xHi*n~Sqc}-Ira(!lw_uZ)YaCxFj6B6}Mq5iNEuv7R+R~uP zl7$`2<|YGlr&@J5I|Ptz`uuYXuvIo#G4zeI#}TBoYHpZnEBlQ9>WWy>F_3eT;^f zsj>{Tk!h5kvm_#iQly89OpVasU(0EzF)J9C29`}YdIukQ?UeN03!^`nT`T+>UeR(V{?T~$?c39 zR6nrz!G1V~%b^Gjx<4sO1*o;`bp`2~aj6!TgOrFkChG<@A{l@bh;M6)(~^EwiXvPc zAXQH3i*8%QW?V!(H(6ofcKcmg9HiG3vXMtTME3E_Bg5kjIp&eDa;zVm2xOX(cCx<_qri?KUxOt@5k_9P<2h_q6#i`_qP$c( z`aY$dy(8|-kw}%2JTg7Et}Z$_jO4f`%G?%^4OHUBDT(Y~S#GjgArDO97(}NLVk`kF zm=SALxrv3bEMnjSf43@Yp4KlidcF z=*n1}Ui1ZKQBslpBT+hT+GCX%7s`n*69w@ng;&?`+=5z{po)bBmWrMhS;-v(A04YL z`K;|hQXP4AYGP#)t(^!BuXdX-;Z_LQ5uo?PaxI~YMwE&p zmp1SSI3mGnR z$HD37n~;`b`Hqcg1waq#tBgUH0uM*xJ0{lY+lyZ~wO}9>N1|*(O1fk66oJ?yAEL6Q zQ(E6cvJh<2cUT+Tt#grUFi{xKBy)C7(}>KHEg}d?#DF+!Q3pP4bzv5=mpJVH`54&2|zbs0Y$1iTx{`RNLo~Fo;Cr3UD-e zEi77)o1!li=LVG2$@Mn+e?pSl$-&JD&xzTNp<3uN&iZ5(dP#3 zCO1N@|C01ZsdegTb&}XArdX6FRhjQHEOHs|Hu2v1$aM_kpX9b)v{or%Id7zay-RFt zRG2Y98WVA8XXLT$UIpKaWd=0Wrx<+cflErZEni z)O2!V$a@Rgh$xb^c(P0x8-w@I$oakKLYC55d99ItB!e+*vQ)%_twGzdtly>OC=Ha^3YRhxx_Xvk;FV%V$dL|(%q3Q&obETX=(%oy+|n~kw} z>?uVWw^1umYPxW=nnHd?XO#exL}6_fvxp|C02vWs5GVFflz@(Ttrp$b7LbYWstX9% zm~3r2$|$k@Wti!3&kR_0-HeheH}2dT0rsjb(#+9=6m6b+jYA3V*)Zr=Wr&uAfWbFG zodjz}%VnETk=*GJtkNqny|LU%Hxo%V1I^X7ogokaCP8-CC1<%H_DZOkHrN3a^ATW1 zMJfXJAk>x{28>W*`H}`h>M2=zi53yg=u~)ofiG8E?T`)#>;V_MnZu~<4h)pe&e%YS zNJ7YYi0yvZNGnlCBGy(dW_f~)x3+X7WkR-WrQ{;!J;cT*gYGh*Hi*CHOGQ%BAXU6F z29MN=zMsknxg{6BTY!JXj4>D~NiPoNV3cfW;~^liHQyGH*=&VKti9Z2Bp3(98yWvy zbSf^;gZ7L;xO|l+<~mS7X=8fMsO(AMZt!`hWLX=$p>vp$-0Jhmia?(i>!-}Jnm8N^ zrfE|oc2GnxM{lz`HBi}Y00zn+4J^oPtPEMk(8%o-Ii>29l7y#^?4+9EM z?EhGMD~l7(`JExH%a*SErU9W7&$l!knYZ)v81XU?>^@i(M70pKcG2ZN(cFU4%G4V} z7gsG5-g-T@WN{A|ue);(u|tYwVhrS*dmUS@bx1X!lKEr{WcypW1}B9HWk$$EGSLtt zG1nKwW+NGdvXhohucdL2scTFE@xGKS+P52&FePL>u{igL$rT?QTwr%JCXuWWZ6w4r zHg;JLWs1!#JcKKmk=)ypOe}hYX)Hph#1}oTUK=YGDSVOsMVN8ao{Ur<`r5l~m5kKv z6_YPw5`S~cBss>2BoGTy%!)(QG{+3?Ju(taL@Q}JCZU$XkUn#q^$xr;g%ScK~tMr>9|#bJ&~C72z0g~kj#!G)G9`1uVBc@leHO} zp_$Q9-rI(UG(%2cMnIM}!X6+-%mF-Mz-}YCxuDjGyG`UsV6x_;O2F_L;vFHHDcr5# z1Wk&H-ZqL8y&lYq3MGW3q}AklL=^X;xRsL~6BbG-vJ1{r;l>2D&9P>OMhS8ID;{nH z#K%7SzS}qa*gqvko0SUd4ea9CiJ5HH`zT}MRpRcUc?H{ZCQ0NktC4bB z_ImnksQAGTFN~}Q$m|ow61HQtW(UO!BL9+;2DsQ2b9%&++0In%YbW^Zg74#Syv+)O z7BRD4P8iu}R^uzZqR1Oli)RVz7^Rj4Bj@@RQgKAL(MaP*C#J=1cf7odi*4L3Nz3PT zIAtL!RSI2%N(&bj5>v&k;ZUt2br=`?$qT;XsV5rPb#+B=w)k{HZ-y!nqEf}`unDCT z6{=S!!K{?L8BG>d+#PpfE^8S(M@V;n#Mi+b0cmuhfxb$(yT;RN5QGD0IG?v7&PZ)- z>(U@JHC4oL&v2v4H{3rgFjVv^sF8-6(8K&D1ve&feaE^|Y=rkw>ao|A6{^V)YoEMo zn6lzE9L|p}-l`{g!#x3RUAiShkSZl!#0RlK^}tYHlBHJW#k?Y*A~qdjs|+JadK2_= z-0fjJr^UH>w2=>i$*7HC?R>ngCx#;twF)w5&)yLUSK~=PRB>u)Yh9v{oY<*hA;(!W zia4thL_r?5MO{l0Uoi6Nr`>I!^-VaAkaS0OYdUP#X+`I4=|ol(RGN-8M5|*lBjc3A z?pum7NQs+I305l|&civMmnlB#_)vI>M>7-kQ$@3fQR6lL6ihM#Q=-DGw&Y0Bkos5P#LBTMJ11YgFnMp6 zet3_XEKD{2H25SXLy6h9aAllHZ$RrAt2qMNU}Z|0GKsA3mHLL-whPr>RGwO?9tD*7d}y*hl1&}R9PG7}YR>6?w~XAlRuZKFt+$>hSQNFRi6I`YBG z`~#ayCt+rV&rQuglj&u=R+&tnguoF6<|mkkB;nmvX{4whp&GNa6NSVDBjs>_!=f>d zAb>s~3_ax*dXj#DZvKlVpMEA|kJXGYl5pOjkQwgg3K&U@@6y|*Gu}d@x7x(Zz$n*9 zPmDa&Tcge4KEs@8DqdR z8GqPdiHU@08QRWs!Hb&Vob3yjJ8tHR3CKt!7U>o~Q-<#rxyyYQRnJ&-?4Gb_Be9@D z`v?q8wH^XpeF`gM|NZ)SJ%b=!UQ$}zjT5M?GuZZHC#jryRlITTF z(XoSm=5KR|joC(1Wo}%vNC519#dNYZ1t4n*ehMMC805I^vvir8%sj&l8E7$X()Ep5 z(j!x}>`7YDS-I(Fvw1mm;OEZ1S}fz}#VTRD;LNi5Nwh%IcuWP6Jk0gfuz0YtT(BTQ zH)eJ~X+eToZbm|*Ng+mvWL@HoYq#;jJv6iLV(S(&5~#uN`)G(o5SeaU^d7c&%=XqS zNljizwEWE3uSt)XBexx=*}~5(N_5#sVg#;t2#U~*1{rV66x)%3)K&uV*<53KOPWP) zoIERhu)k&8U=@5o5z*s-P_^ORak0;Wb-U?jkzGPl0@Hq4@Qq}pVC^sXg7vfcQc>^~ zPwnbsagU9q5{G*#EgpX*3u*EStDuC8pRjO+H$Ordr!;uEGkg|WrWhlVL{9R>Dxp!9 zf@&47NaeDF7L)O_gF+D!j-v3^#=}6?fYX5^Q!;Syhe3n5OvK;a!}ZOO47$lr2)ai^ zg~(+JMNDROB3Nuit$DH*WF zQYfPPWBN@%Or}zS9M2@XB=oOnbe<^+oHNEG@If%sG7W&Gqc&EdjE1=cPs4&@xO)hu zfm&+m%Lk^Sek;<|T0IsPBrHBNQvuh9I0wLdOP_@ig@j`3BPLN1laQ#0!%=~02p1O( z!U>c%)d5M+s2vp18hCZlYn7>rB<$xY((u6;l*%a^n-oKD{lb^9R!eRc#3tdrdDPL^ zBwZHrCXRiNi^pTV_>+9B85t7T)s0)4$E9L@p^w7{EEsjd57^~GXa?#21;LP_NY*tb%#H2+lqDk~9Y(@7Fxf^#{LI(cec&(co zze{K69LT$3I)B<+%&6r}_+3rD+w%{TyYm<>Hx&A4u}D13+4zk%Y3vrUhG=pnz}<~I z!sGSiQ>=;&Oo3es*;w>P>#@iixtp+m_g9N0O)fLer~E^a7knf>oMS~xnO=PCgphQW zNqP%D6te}Rn>(Hu;`S8H&Q<(F-P}b#C`jLAQQiW5?P(x)YsaSsHHGUw-Mj=L=AVrP?^52+{DZhE~pIhPoAK)`qQ>e^9i{-``N-{6oWK zieUT#(nG`L3T~_o`4XXWdqPds^OO5?b;qnP@pa>aJq-z?ryRLT9+OVy2Y6IJ1~ouo zn00XV|4UMcY=e6Lsr-U8choCGmul=J0%7vi{=dAyL&i0i1zsXRKILtOlo|F9aWci6@0n7+# zrd!Qu@Pu`Yz=Rv~mj8|Jat{Sta9~oOq*n)qkxG<`ZT7&9>Tq{bU%n_=o&L9}LYtz? zD6U>(xkxKJYicww8Z%ZL3ZV$X0~Gk5pcLG&ROebnv=YN?j7p)5Lvb>Zfn0Hli}LqR zk5Ghr3T4T?n5-mKoL-^D-7;)=*b^bpg*10g7rZ8s@FfEN+x?3tD!xudYK7milAR;tYgn3yAb8o^4 z;?|2~znE;vD%z`(ut}&eZpDT|HWlpVW=hZRJF_KbGx4wF3?~+bP=;kGw@3?;H|5A- zy~)K(wvD;G64Ty8)(k5bv#fA8*M{0$k*KI5ess0&X{r`iJfbR@Cl5=h0N6&U=Z@M40n3LEo*gZ-_h{5Gs2fxGjm&|?RpFP}O~5u5~3 zJ{wGiMQTS@DMYQ^ypYG7Z5&Oko0n-l`D z6&mX1VIhQl<5J;aEw^YYl-igCDAI(=FotowR&$!2`Gb+=Gz;LkX#Zu}d2&ja9QT8_ z8I9B=n3F)a(?leY8o{04gWHpG8-fU@^}0mtAHl@x68VdZ$!gs*ma!F1tNCiQsX%-z zUyWfTRu8MZ##oIU)}**wtz!bC2{5xdCJ5wm%4+TAO#Q&e+5`6|mseKf6BrK~;}ZmO z@mbAjVlxRgRx;!-E;g(5#yk?|bhDZ=?%sT- z4H|+7r-xNeVIFKSs@r`C|;@c&%z+q+2Q6UT>c3Pq5(}<0Ij?%xI69h$b>fC->oU}rbAED zxofky!jlrq#1kRi<#d(6);N+l=)lMI+*0V>D7~>`mCPxpqp@IJBPl*Qa}aQ`g^Uzt zIh~$TWyz{W5);iswtA7oLFc6=YZgg-bh2Prtw`aa^?|HEq)^K#xv{h)Gt!(hj^I*a za}#DaF*?p2JF6gqMTwM1<+MU_OAAT#tQw~2)G@Tgq;S!EB#R3v)E3z;nUSA{!j4Z; zL^Q{Z%MU3`w6?AZck`8n6js*P8J84NnAk3^v7NH@g%n=4rEJW_FOtH> z6CT6_#vOhU}pd4FLyUi3Si4egtv>rM8L#KP>c!_VjYec+~mAzI#+u9Spfakg#TvjrA!G7 z%-rUS$v1gZ!knd}Q+ZAgPN=&F!sDpvCA$zsud8#n!1z1sWYUjWRT`zE^D2@+Hg}>4 zB$5P+1}YwtvuHwzcaykpB&Ad|cUHwXbqfAk4t-+}@wp?l#{6mzb6z}?AgUTpr#R8% zOb2P^%Rz^NC$F}lH`C%!KdT)vYA5korFSD0fINod9S=jrz&Kn-P$t0*7-wiIu>j@n z@e14-C$$?{t)l6})HRh$G|d9K2X4%F&YK^vHf+fa1tRjcg5V=I`q?8S)clihs`;Ft zriQ#ws)}j(z!0C@cqf4-)k1IvN57J}H7#;u52Xl#Sp#w99j`E1DUyR77;zg?z~7__ zAO_!tP?Jkm+&vujSb>oQp!mUeU~49x$R$pf4J7A(kphIv*a zSahYs8)KY40~=nAbR_#6g1ffs(4m=E*QQMzJU*1aQhD6mkUY!Yt-9SXpvSxO$GeG% zzCZL{&?L`IMLV`T(dVjnm4|hiK=R zUwB*j;;Onq-eKpXw~e^w-S}>qe9`&VuNqlMnSa3@*PE(xYZ}+MKjGrBfbHL>m(br| z@_uN&DYItpI(vQE(RmBM-#vT#wV#_+xx8rFhCP43NP4O*r);%jSaP=Oy8W-}?Rc5z z(yPlK`d+`3F+O98Yf92nuNKFi+^tpqs%Fgi8TI{}xQ)G9@Yi;7->{@BS+xqj-%xgm zi=Y1Ctu9w~)~Q;1Qr*K(=PW;#vwZ4>va#Koh3xr#;fER1evY`bbk(C6Cn8fP zUw+eKs_LXm-A;Ehix2-C`MLG-zbjvoM|Ha~wdB~1W7YT0EZ^5{`9iPyH_B$|l3Jfl zetope>e<@h-LuC}@}3ZPwa)!AJqK?+)opvDZk5OHkNFtdV!>6flVfiD-EhQ_lB#iU zs@!OCeQ~!pm4{B+@_C_dVzu6v)_nJ(NEPklKd1DK*m9-enm*4uM(ydc(rw3+cga~# z@1)gK_1Cm}dMtTSzm=nk-i|$UtUnofOra**i?57%)1-R;`&a&% zJpa0`%*c{!63c(8`e4MsvwN==_;re|*5jTfKAmuTG4sjZf@A+)7d2#Kr2+Mtj#|{B zO50_>JY7=w!gnJEJSpVmIqu&1#h!T+3m-1CzF_%@>hW9hE*w+lQ|75MORG9W?i^OM z?Y7BTJqrwc5j}SAp7B3?NY(X9QiewCpRzr!ZOe@hi&qL;sLVMNJvg{uz~Ref%6Q3F zzMM6?dbItj57o_yj}{)6o8nun?SLSC9(ipW13^_#Zo=Mi-hg1fzJRA5SM|C!&< zZN2)ZUrP0C*tGB5(vjVdz29>6duM+$yN1`0C7NnCr?tLV z_|tLs%shi9_PaZ8%S?^O+Mh~1d{|;)+Y+^24Q+q3Z1@p<;*BGjw>EA+QRG2Nr{IyQ z<`-U68=l#8&#LzB{$2Y0yf-Iv+%?CX7X3eawn)9DfBkZrW5Tehl@@+`Z2H}CB?o&v z?H1g!|JvCn_Gi@WeHmR1>2%iONB+%&UulHlABPAIg{88D8$Dg!5%!XAD`nH}@c1vM> zUirzEzlP1)z4A)*?1L|QH(gQpa3?RPhF&eaKJ1--_4s_$^i-bxG3J$q^`0V)xRr2Sr zJAbf)Z@v8;8@KNHw8QY-zRi0-{^VKHM>+4C`W_G7>6GQW4PTJ%8SqJ6BDz#gvnhpJ zrQ!YeT6N_owA&)wtLp}5)=I4ER5>$!!;~d6Q>N8_+iU4ar(JojA6qrS|Ma*z_1z1< z{OGu7XQ#2-O1ByNeDTOr?;R#~%Xe(g)u8^tr|W6U{pvsFgh*Kwip>;*gbp7$ENNn7^tZ+(Bx2-W$Shyg0r7Z@sgZ48MElX!m+QMPzOJ;ZLR8Z{rV*KQ?evTAR48^ETYuw|P=f zsn_rBo~~7TZ`--wOupUabmHEe)Q-Iu%(z)y=`r|K-jSV$XC3u+&Fix|&w@G;<&OXP zXn)v^GP{daoU>E;>$!d}s-?^ryxsfv^9fs4J=*hV;krVXI(M4cu=|Q{R}a(ls6O;` z^q4l&Dk=85cmJ+nM4c%Wmi6va?1!jE4QHQQIby`tsJNELf4uCvsN3tSW#?rsOQ_dv zOU{Of;17qlYD)jyGGCjDk^A0OdRsF3@YT{4=B~XGaUr_s`pS#D-_f7hvfyCk_x?i` zG;A~bsBpm=en}3wnn<-o3_aqmH>-w;yr4?pjBF9+=&J+*(kjdd2ao_OM1%J6R%y)Wd|B&__l zyoX{ocHQ@~@UAx@9^Z#<)t(<5P~+V%!2{Rq&1kT^&AO=P*3#ryWAahfrur?!R z%1--D$ur{U#X311yq=yv*kI|I_VVGk$J}areU(eVr@jZK#J=i3=Ch0b?C}FW;~x05 z44kHM{p(n}Un&+T6ui96*4{sRRc*7h$nfBq$;*ChSRsD$>NC4$RI7Jw$Hfwj1Exoo zNxd@UH;-kyDGTaVXg{!?%e(LWd)2MCwsmIr0k?lWKD2N|Nm=HcQEzU?v~Binkyp_^ zzizoU>qct8H*3oudp6hihfTk49-;}Wzxq(&j~-1oJWHKi=VH~ILH(6+HHJO8Qu4#` z)s;(T4CyrYbcOB}Q{Nsbwdac0F3(dnPqyt^sh7)yd<}B^hu&5B9lq6cU{;=qXB->X zzO&A^Ys+&-R>bW8ZuW*<(`sE^Qfj~rm(82Y_+7dj&}-CydfvfTCnVMEI<;)`!h4E# z%{RXE%HAJ4Ev#|y;^tSEz3yqF{cHaI!X;$gV!x5S4)$5>c(JN$gTMcYQ!m|fN&oKE ziIB2CtnGK?W#jGIDb96&T=#KTk=0=x#x+Z?6EOdCodsLgEW7nO@2{(qR$W-&+kC*a zO*gtU`2D=Yy690Wel7VZpyRCI6EmXJJpwmYD4tpId+nkd_b#6vx3HX-ruOk4ejat& zY02I0&x*P1T3Ww+@}nzL-hFDYW=Q3^gSNf9QV7FZjabmVcoS59`(Bw4nzvn<=`QBEEwM<=KJ<@zsKyz}R3g$W}Q z=U51g(egsF0xo$Xon1;fMTW~u$x4tv3YIE}^+}S-H83Tnk-WC7CJ8K5sxl4qCGShe zk*CLTV&+QTqt)O#ahAM{tTds^Q>qB13&TBUS6^>gm9j`=2-7RecC0SU5oBeaY{y~_ z2ruZA?da$*D!r3SkLlh|7SyQkG+*(TuFapGE5{5k*v6&i;6+o~JnU3-^6zVl9^c>k zc5=BjRqu^zxBagI`EPxWn)>ZGcYm6-$#Z_eBWwS>exuLJgO9$=^jE9|1UOLC`#Iuci2mDlaM*G)=x|aE)#-2IfG#hlV*Dg0jv1?(2+dkN~ zJb&zz9tSGSyglS#)xLg}l3MRxS-WcGM+1KEozu5fyCq#q%GfGYjK5YA;VT0#4%SOnGkxN(d&sgaV@zeZSx zHrApVHA0ZAD4{P#VsJ*K9w964BA7rrVpwXrK5vSGkxOROM8DE-?ZM?HI)nc zZJE+w@Rn{Hmn+}rn=r|3)9LLkc7!(T>!+Xhs7KY*+e%?icKj%v90$j8E&T>}f(9qj(Zp?}4->C4xz+uZ5u;)=8Om;a?hp{V%AJ6o?` zvwK+cry0rm-HmE=elmSQ9!;N@3)(Mo*}1vO=4xcybXz6<+PjnsgYDLuU26^lG|6b^P$0bh1JeH5#r;Hrscc6|g-;?ay zA^)VJAJ{C*&NH9(1CCC@Jj(mu>i}d;WFBswZgLNqdlPg3?w;rXnz29sE1i(NmVRsE zk^)sHUaZnC^kLq|y|acleAr}Om!QZ;W6F=Z^0s5rKlzqbmpR=l*ZNIpfm4IM7f*eA z;$Ym&nu#Tj&);~Us z#b&j7Q9rP5h5et7uG>`feD6wU54Bp?{I{3IiaJaxw5nQ}M*1@?i(HH@vuu99N$tWT zcl_Sid(E5oH&c(?nIGgosA^DR=+EAxcI%@H6(8pPgZt>At7Kgtb$50fo3Lhl0aH5Jd60_n(8C^}bV3^4+sJaOPr zkza}{i34LDY=d0!*hfk%c3)k|)nzcG#Ja|4;Dm#-9C6jexu)q<4NM|R)qs~o21-Nx zWYkQBUo3bp|2Wy=3KD4lz2UN`FP#XK6@+nDA0J)>NkGvpZ4*Z{mnnEYR9{dcJKST zNcOQS59c?28{W4^?4L328Lyk?YY_GDLa}N^FZ`h^-)Y_XCWCkU5P5zPCfGjj&z%;K^`I@dwORFZ!&N~-f*eEuQ ziJU+7aj>%731L=|Y0Up{3PEN!o^rX&UEWmYHfRXWc?x@(Ont}_bkbGVDB)Xv(pJbs zE{T)OW$yo5fA5T4t)MWRqWpB+sk_&7&ZFh%QI^~Hd53W zSnTfaI|{X5{&U@cau@eIPjY$kp~ajO7ge!xQ?&K!d3707w#bs-bn%s*Jq&2~JYVyf zCC<+;e(Te%Hf3He7;r6QQz4gb=hnPfRxvK%z~%HUi4PoSpPJioi}R~jWg|lKosaYC ze(6aS-O+=0n>Tpdq~zIpld6t8Q?yKvi-XSow({N8wEjJc9G|n@N8>bA9?)&zPd}6y z^S5JWt8X*QME;<8alBI3IW9T>sHzOR(nL{tb4t&aI}1HaYuW8(t*I9lF7X>sGVWN% z$Qd0+eyotw?VI#tEk6(H@8@}-K;ZM0qq1i24jw($^}(_#RhO}j(a8m~X(w4*zMpY) zboRg)CgcB>IgY5wrZ1WpU8=tsY0Dw=kq#;!@F zGx#P^`nsl%f?c z9N94TzFYFbAD@*jKCEiGB6vx3OP4^GyW0+&U)|tVYQC2%e>+$DaOkUUC-uGoHC{xV zukq&Zh;I92$q#SV`xF^H^Owsf`ZQ?u>Cf_Q?%jG?C#Kz%*pR+Ar&Vt<<@Ai>ts2w} zDRys8+Ry660e|P)m$ZIinRX|_7R$3suaISzUW%ouZ0Ps@!_L?#zi@yMERb`uoob;& zd3N4kZ&|fl z@9I|R_`rK|*XKR&m+t&lb=u+ZWcfN-4?_zq;3N-`H59DXon7}W*VGFCuE|{4lVf*Q zOfOjb(b&50ubgarN8b5yt^6Ht&P4~2w|>z)P8FO>R~}vc>X0Stmw(JDbRg=)tZ8X6QB9^ncZjT`%iRClc| z+G&7L*BI|IVXdyuBKHd#f}v5YKDIidq0&v%K`41AtaZTxEfZB)tz%*n@NlI{*RnDB z3xP_lR!KmCmgJc+0@`=esNFq$Q}zA&H`92hcZf=tE4?~tVp6>`S`oU&JfVRjvq>h% zLcBuy`DZlMHBQEW*|9@fR&d8qoe4+F#w<3-;B;!)nA5m*LE*##y8*~rhqecUf4VjE zW1SJP-@iOOVd5Wkt9<^xZ{b1TdZFCEjsx51AEo}DZ^ZT;G%ee%EARxEb?r`hW7-SfTN z5w;-j(bp#~{2^O9#i{AmPi3#2)K_|z_s-2+#L+yRxriyKPshdp&$zpk~Y% zdCz8J=B}9@eJ^<3mgz+@N^K4LW^A1~g^soU*tTJ>>5XnYol^Yl@tTz;xwLvWas11V z-~1Chcwfag1)Ar*ccN2bts}qxUAFwCfoIRRe3O+@^@&5NJMq0A_IddG+4`mX?Dpuh zVfE4+x6OB-t$*72(fbZhoaSa1KQ7BIe%PfD+c+vv+^MKMyEvAW#Wxt51~#X%i!Vmd zuTG9=UD#BjwHV!Ke*veXb55eOS{9G4!XbgUd#`?s zKXcjhWsTYHigvAivDNqKZ;agNs}q$cXAoGBY~+{9j-075-%*6@+YVM z7dE0WizMm5GFj${wJ|q~#FVlb`%~s4uYz8Rn<#!`)QfmJ!d6*GmyT}yEU@60Z+0Et zaW>H7kLCGyzF$A*my5r?UVM1-afhNWYL~Cn=jgTIcW-C*8xj_lXMdw6rK0A$eY`ZT z*lle4CBe z%Fk&(E9%>kk5&&kHLPW|c{OIe8B%&j(L%#BYK@=b*`)N+(m9FRdP-SNVMS)kwIk0? z%jkYj-G27vx32p(Usb(5aI=BusjW|H-l}oXd&A5P4Ni=SbkPO1Dd*qSA^Pn6@%QWR ze|4jCo>fy9Can1MXv>WgS6cR&{L}gKDZ}qgURv({Io+tW6-OO&tvYDq@QIrTCAR&= zW&f1p1$RU*xqMwWYuo!R9dG=`FM)TUkKJlxBIa?VR>+YnO~3b=+qL$aw>Q4M^1RYy zuy9iivY^e2KQIt}GK>ObJ^NHvm!EE69#%=TOC?K9RgCigCsQx+saAfOJDGx2b#5T5 zC##!NJE!Ideta?A_Jhwg+WNS?KjVJG`*BTI{(bjO!Tc%typJgs)m;+)?zHKC1*Ul- zleBpi2Ap27G$mqyr}{;M{--A#?j1j{wCn3jjTbh17qY~$OJsp|a~IsKJ~3zeu&`YX zU1DWbJ`O55ceh*k;migl9n=*&pX~T}d84=;<9n?4`Pi|~5ALHHK74g$+AL+?>-~=A zFE@AJri;s~H0)g@VM+PF4~%>A{^SaWanWL-UnRdDBH}y3of+zj9W>JGql?&|Z zRr^%b(^54CYz=?czyIFheSYY__gO)#qVr)1^Ec}T2L^SCne*c52D7aCuk6Lq0|5`2 zCpi}I@Sq0*#y|hZU9r6d@u`e*b6-y@9^dn1quCqB*NAWZ!RJPSfdxlzEcZdzw(-{8 z=R)_?+5c&yM`})yQZ2XU51Bq$)2!)**{Xlm#=M-cKQOtM>#1fR3;gl2`HQ0+)UkP| z1zfw}xm#7P%HA^bODfkk`seAWB4aPLdgXFVGon^dft?li7tI{`tE<0}KX_DapRC@m`rP#II`@6&z}}NO1aJELv~Jx6=K(jv<{X+| zzSr`^sY$bMJkmGxYkf`GI-ma_kF?rT+s;Uvy2 z|GCmHJl}yD=Q3xgw!ApgIF9cWmvFy7%8)P90gT*^7(rGk0ly&$jaE{L3G| zRUS9#WZIZry=MM%XzZoFeVbo?7%}j1>BJXjTTkAf+&@C`+;?2@YPlBKj7}KIWV~2e}2@_|3%2b>P5T!RXyMB>T7?vKl7CTp_Dea$8G!& zaHi*hmCN!rPi|Uw{^9_YCUS1V$OmDq54=2m=Utm$Z`B^Mu6g3wUAN!54c+fHul@CL zF7wOZT63sH$E(*47hV1P)Dy$Lo(a%{Nqa5*>zM#K{rh?*@bygK|5axK@@%Izn8~!@ zwwp%&Z)Xcm`3>tyUtG#yJd*D&P?qT<{re|l~l$|TIwMS6T=n4)&=gQnUKk4A}Z~xpL`Xtesk@=i}zTc&|qt^F+JWSu6@0!^{)j6`qV9SH)4Cn%+-(HUk|L2 zacoHC<{_Wl`=99;m1pngnT=NLziq?t^6RIsIq)?HzUIK!9Qc|8UvuDV4t&jluQ~8F z2fpUO*Btno17CCCYYu$Pfv-96H3z=tz}Fo3ngd^R;A;+i&4I5u@HGd%=D^n+_?iP> zbKq+Ze9eKcIq)?HzUIK!9Qc|8|DVo*WA7e+!i$cM>~L|)PiDz7McS`z<} zzZs+f7W&1&kvZ<=(ytc!^`AvQTxL&U@_#dOQ`!Fyd*=feRh9SuJHQAkI9QlGhBXx$ z7AY8*C{v7p+|fZ%@lORQ3W_TV!AxpdVc^Dfn9bd#KfBqk+s}4=cCGBm%*+gn1ueEU zW6R3Y%H4*AEh|T=^-nl@yaN)8g zOM?E0k+!@GwyQDiypVJrl0Tnfe#GBPrN4XZmumIxcv8KT%=OWENPGBWs)2=g-}Y+! zsd4-@-ldfgY4sg_O6A&Bvy$jsKYwNA3zyQL7WL^5wZCo6s(iuI>XFXLhWq-E9j`%x z3m4{C3#~zv&wkBZ<~yfXG2=saOnT`^AGAYdpw{}y(C@he324ikRjuml;1y2i`ugzpu$7god>Xa-%y+8#%7c^y1w(uS=~%dM zaZSLhoZr3;_o(_FQ4cKH;Q9*^vuk8`GSh3f&6u8>WApjYO`JT*kG1M?)931uiqEew zJ@kIuiTEGym(x}JZoLA3c@}?*xC-wwA6Kz_F?)me!jh`xV@ucKRJu}-hZ{;l@J^0|1{r?2<_MdO(aZ z{G8PT!Xx>4b3(XuXf{qRd@j_$g&Md}0~c!GLJeG~feSTop$0D0z=ayPPy-ig;6e>t zsDTSLaG?e+)WH9N8kj39f6i*Kj9g%lieKGCXLAZ<>Rq3!Pr)1@9zWbgCyYj8)H2#*Dtn8PS>G5{= zil|)Et`n)Jx*}!Q^!Th@2CFMlR$jAb{ME5IV~W{XEW7TEk~erAF|zWF?3#q2^Y2|I zS$Rix&5xBcCL!ePk(D{|vTJS9#+gxvI%_{Nn06hsX0*%7o6cP_^tmLP>y9Yzp{_?| zrOk8f9!c^X$5&UQU0H#6*-7$-W=BF+{VCIf`%SyPuq&roNvuWIV(V?zg~i_Eq35iA zbV@jkl`uO{yo1QGE0Sd*7U{)5gJKSsl~#MkXO5{E^UO}G?7GKS%x`7ajs9YumR+k+ z%o}CpG!?T3*14<|u#lSpMPlLidM7 zs%|52?;!|&uL_*aZP~P=Ewvgig?2LA_*?yrt@bC%E$dS@<=c(fQQHiTxryI4WJlTE zcSXZK$d1S1uj|>|Gos{-1CEJk&x``IGeyo=kmN|0T~(-`Q&c}EWmgJ z6f`|rGvk2sb=9!Q$X>N&OpSMrkzEDnS1s)QmEfwiDKp~9N|n|?{LVk3!I&>#HC@Qs z)TX?ncD_^D6Ae)AzAMG<9+i!0xDnH)w8`$e267on?rvK~ zo2mXFPI6QKW^?74WmoE$}!W1 zJg?V_d^w`*%IBV;$P{WODaeS(gZeGLKy3z_@(-%le5Kn~*UhTcu6*jL!p#vhq;380 zsdBmolWZH4eMs(Mo;~9pv$Ft(6J^i1Bgt_Q<@a)GdDj3nzweUdvfvWzWoB1}5)2^k z(9^Yx2?9mx`nhU1J_pAg{zl{PZbQFoc21GqmMsW+e!mtE9W*<}0$+4ggM=ZBdeug@8hXKz1B{T zq8=X&S9Bb<Eo(GuALSvc@0xhpJuo5W|q0gQ+Y5Xqvv;*Q#_ijU4j0|%Y7I@ zvP9P9JuTF6{hGeT;~#XbxO005^@|Yd2&bsuC}#sTQ?Fk$sKzkQZ1j@g+R1@tW-DB$ zNbMAN4hhMf6q371{xW#b5`mJwN1fJR(zxI2{k{x#6%z9XvZ3R!t$@>KKPgJh(<;h^&* zP0TTcL-5bCdtMYJ_9ZIMu6*sGKdVer0U9;9H}uz@+-7B-LvBMIl11MhD=SbMF4s+z z90Rf9kd<)R1qr;3@`i{0e6sA#j#X>XzrH#nXSAA{rp7yLvg@|k+PmT<=S_%$E@qk&Y0Zo0gK{UiBXADJ(W4)IaFcO~N-ooE$%XQ$`G}B}wNN_~(Pj0Rk>S9E z!^&#WAS)APB`;QOEanzuL^V+f`i;2;q%SHg4MVUE^B zGgd(t0mj+~CC0>bhk-8Gu>5h2kTYJVqECtH!rCV+^?f-~oG)_g z51Z)heCj8-WA3iXJ_9ZbzLhz!hB|5K(en^pUu+BGCnAVPekGCkIg@+@bj=<2#H zMDbvzI6KO(8F`?&H54-~>zU%|P<+rafLZs-V5NgMTI1`RO&e0-YRIy?P=jw+Tc^ew zSmR;+%(66qlsTB~v)HippRkDx**Xvud*jUbhUxLJcmvfB#qLZa1X)pIUrHoh^Jy67 z*MQ#JtAep0*mxAYLR9@vEuMFS|1rgeTTMgr%-r)h#w?Xh_cW7dc|(Tpsqy)q&$}-6 z`JNNZ_e7G5`uF%B*I5)h)c*wgVJuj=BbMQbYy6)0i(pUu5fy=X;sG*TaShJkiq*RK z=lW;`X4}+9GggN7QJ3-B*|ft&&c|iEDkyw$P( zwO<*WGiBGv9aAuMb6t-Kc{N;uXC|ddgQwS%qXuzhHC;)^sb>I}VJ0%le|afgz67Us z)JI@=pltfreDeD4Bn%N&C_h6t8t=ek!PcDyvh@epwDPe- zXr_G#{dIf^d3-FtC$4{7nt zxJ;5yQ*y7f!~cBGDn>CQ&WK5*LlL&nQ+H&T>;v)V@SHk(>$Gdb}AF)R1707n5<8Zfouz znFDKT&c)VVBD{y$wxmryeZ;POvGL-Dq4kFy(RTOk2HE?n>}_^-wjyutkyEGBft8Y} zqDmT#6kCngU^>p~Y%osi&V;R-AiqGB1`pzummHG!A2-W}18lqGo3M35Wp~67)2?+R z&?R20^Nw@=Mc)6dxi#W4$|$Q^#}#PX$tw?;HoOHbISt3<^)1wzK?{7l`w7-s;{ec- zeeDY50FDJqf+~Yu`A8mveZ-H^yz=_5CD^yA{x57+6|;!h-$2>DkHqpAY|xzCIMmt@ zi$Y_3!H!OAnC#szd$AN~c7AMam88z!v8d@M$>fkO+EaEvNXe%*Q`*-f4`{--Z(>cz za`OYks-{@9@Ky`;OS4)>198oB3BH)UVi;WCeFzF{%9ofEqix5S#%w4+#hJ#q7qP>G zRqRk)YZ_Cw$uwr=7SkB#4$~Og6>-bBWLGw1;UFvSiR|zot-$TB^c#_%eAfi*BVh@@ z!OX7Z9+%y@7(a85wB>fBAlbIuL$Q>7cm5%t<=d6Ro<+!CYaJO38p7#uMxzBNmuv{| zELhHS@7rZY8RF4C7$|Ji;r@ZzXugqZ16GlHDD{18)tKtn!6&pDy)xNKo3SGwM|==` zGZ1`(2W@S+>Q(n+WD&lsc@CFLlK*5d8qnI%Vy(SNa@b*@8?q?L$+Sk4QV)BUqk(Kn zE;EZsvg=~>269L;RI;Wc`yGZnA+wjV%%+fJA5yTn?nW%HX~R`eMe3cGd;Sw7I?5%W z|7Kwgjb1kv`aF$@kz0Sl@dMt{zVth>hu}+p1!*zW|BMRkHXdb`5`_%C6UY!uii(=r zOu4w5D#>N6<0(;3Ln_wRNU8IOq{C$__h(`tWg-%nQ{V3#Lh;<`gnT)bVp`5J7n1%I<@g2$8&5)%iY=1U#KGMdKZT&n29;x2W`=db*h1Q@ zM9s{yKxPI7Wya`fUy7`8`@vMuWlU!zHV<I}G4Xu6u z-JSDSlfPrYT44v#oXE5cJkLU&N0?Snk>O)cWpn7_QxANnr`zgdNc1Z5!$_ zi$ZLSY7IPu8m^_u7n*j+aI$n$eFFf~{AReLEx3ZiCLG!}G~wHfOR%op2<}TF{jj4O zkZ#+Cy%3S4ZNoN%*meWg?0$~oztC_ojZtLe*tqj-96=*w2QGw;kS2_fv!QT9v(?np z+7+Igid$I6Jhx(*WS@NZac5Fa!$~wFl?^V5w(nI>OJ(q+gChEur7*MY_+l-KTP!qx zs>{N?br`?StgKEVHzeu1E4D)pifrTC>QlYNopcsx z$-3?Z*w92vF3lj*VJ(T$yi6&ZHr$Mw$Z1I5gEMrUXW7Bh$PO_H_^m!8CmksCi$*n)-NTU^|JAFJvdj>&=|zkci0>NqBokOZROPZe<`4#XIFy1#EcbT>Ql5p@-|)VvzMMpE%SyNmPvjDW zF(bgty3JEfCv|7M&ZO4d4w$a9gNE+Y%6p!z7qKXl5M>Wy!<0{X#uM$fT!wn{G%pTP zbFEKJN^9;8)XjVH`j0WLlENKKp=T(}8&N;03R3q8sc>74L*}_XSlv-Np~4 zj3*{gGt;Cs7t_zK3GlUVsv@lAuYmX&EXh0-B`l-Eed)fPYd}&_Y}(pEz;pnfWO(G% z_q!hQoL!_^{maO%RNQ$MgFJPDIR!9bucsTQm?RB``!>%=b`=PM%@phiFq+D zsRN^_ejhG#x9U;ds(PqZ4^`@6k$Nal4_WFVMLn3+L#%p`)WgY7nX=>R;gEW0Ru4OH zz~qDu`y?tX?7n3ojwt3ets9<(24t08_uL1$Ro#Go6v15FUNEILcb*NSXvaMFL)N-s z2PP_#=X!*(yz`{uNOL^wh>R!g)XT>pU|WK|=iWw%*L@vkYP^^((v)z+7Rapoa7y!O zQYO2YY=$fOB%3R~OP4UU-UHRL#T$t#yZ;Z6srKoXBWPL=i!t5*JW1V+M$)o}MDmzd zh#EY_w98@k)_rSsMquk`&@{K*j0MeWI}EJ`i6z}Nz~=ouHtnn%i>3Pa9Z9BLXY0II zJ1>&)poE<9PR*jWh}5f#uD-hK2qf6+d#7ebLqclTYd8zT+3GCp8W>u^wC1E5q*lY# zu*-;6tJjQG9+o^e$@|aTBxk(hh_`uL?USFQdO#hZCvJR^H z-q?b$)WfU~+1q=C`)66k5Lrra_N}%MsqFU>XjNw&8wX(h#`NDj%?o~6HZ6;2! zH-x*TjZi^#h=CBd4!`i&afNmt7UQVAN}*SZ(2w6Y{j3?j;v2Lq^A(fJ4#6_kw~!kN zo;c4W0X5)Hyup4-H&IS5Tps*d!9x_mz9yv!Qs|grjk2$+<@4b=@i0-x$!Dm z=%6!R!E~kH;g30+#l&67+lsxT{(-3wTUhdnthIbY>6_|j;TxXx&uP5JdXB|FP-l4` z0$lE)%dU~PrO+re7~pK^-cls3zfxFqat<{N%tWWRiuKD0u>pfA|o`&B0{bMqHdF!>dmZ7 zR2aIn?<#*`#shL$Mx$cvF5~s=5R-<3X;;h}q|}{$fE|WKLTL5Aje?Ivh?VMfXrT)3 zjAa`cmUY;4!UX%r(X4*%z!}4~0)w8xl&7EFjId&y&DM#QIV$3)#N`CKd?#IYStgTQ zF{TlP`7ocsLK$s~rJIN>AQC1pjkWc0VBcUlMjr%E2ZFS=E?!wzivS{f0=EfH^l*_AFM+-97DZ~Cx@hFq2v zJ|L>icoo0oO8*l=0BCMCEeP1d0p#X#U(jVqA_5hGn^BS8O}n8R*G<0bZN{TF@bbDW ze@3|oHG1y<3PWL=@enVlI*U=x(4%o1XgQE!{24BpHhe(pT*F#eM9ihx(CD(%tBRZx z?5;5M*qPSZo@92m}L$7f+|1 zQjXy<56>$wbv4xef!FJ@#3NhO94wOM!=PtHP+p`iIp(J}w;Cm2(hK-6os>=cWB$tW zbq^*6uG$bvUybAhj5+J zE$*CnRGl9ecdq22I?G-D*i-zmH?r8r=Thti{@9iN*lr#|W2|I3^vb3j%QTSGQdibl zMw8MtDQ(6P2v7x~sbp6zttwJa(KDJZW5+eVysl@dRUnw-i z6{UOPkigX$mVH>)IxmN=&H-4(Bi;soyi+XRRfxx{5sUdcOB0Jd9D~#f@~&95;EvX1 zvU~11@0(p%Q5tjn3KC&{o=*zdz~x`w*I8yW4Vdv6Q<(uv_dP?a=7Ju3 z${6_`hAD>`(MPiUPK3vq+=Q!_J~XJ&}YvJ6ps$xq;}O;g;aP}R9pJf0TU}cu zfd%Ze6&vb^NgL|wfhOZLobIRjxqEI0ZL!jt}P>LO&%3Z7pSOF`c$2kO<3aEJ1GX)oddL{0GPiDT-F#)D{5Z2{6KvC)D_++C2dBRSM`>t3o zW+cWh$^9s};fIvL#S_0(G%SpnH0~H- z0*P!N^Q0oXt=WTnTVf>73@kDJEqR}hTN>XX?{j@ti-jD^yAv5~%^u!c-m7Ndp+9`6 zRaMW8TiY;{eS_dt$BocG>EwxFvNHNXW6)^!yT+ zcqA_QJ^D%m9(*=##(gH)MGknJUo$+x7oKa80lvMZ?sEHb!hYKUTUY?4z6Uq|t_}uy zOc{!ndq~}8qHTBV-qY5&y7O;F&av-88}xkl9`{f4;kq_#he+MMe*~c+R739PC}POA z{l}iSsF)4(WVmBE_I+^U)cxE6V0apvcJ*VC?R=VSxi+*wYE>{%Fg|@H6_VeDQSa$e z?t|Q$M?xn2B`hRcrw*$ASOJUimC(R9?vD7L9ZmIODi(I!>uuQ2pG zFYfd$w9a%7b79_SSMP$qFpk=-nHuXZ?A=;1VN5+tXOoVhr&AZiXnYZFBdS+CS8=g@ za@i3)(&g*${26>tzMVf8%6yx0aot@}5^fT@=imt#WBV1D0Z;U#gMre-y1ODI=auNz z5xBOSUE4>)7%inAU<@XO_^k(Km4QA_PZ%#?4VinWE!P7a3yIw0bi{~b#=$fu7q^3R zw?N#CQ;4@Fv4iJV*dua9D!JXb4Mx8raL?WH!tTNB{)>m^ZEzvsuHwDS$1LE#J7RW@ zG$+ziUy4^vxw$Qud^x3jh>mT!EoOE&vIR+a9-ofcLhezde{mez3(u)3xd(8FP;%RO z4E(N0$?a&1NTlf4E^Xd76oEZve5D@5{UGZLhp@@#xeuD){PSL>dhyJ|88&gq>lRbc zJuG7ZNsi$@z2_!&@tDVkBUaDkhQ=dE=E$DE97We%#saLUUHE;->L^btB-XY(>=8=R z%PiwH#IP~eKd17~MGEqjHZ}=)%ch*euuRV+u0Tvw+C1!v?uPAR8WM(#=9qFkR#y&2 z@a(8u93yg+>}c2Y2wqycvxFtdXSwLmk55y32A+|Ogh|A>rpN6tSSLp(I`?&j@j))| zQyA74;PWxt#jSPa(E1@%zjzK&nGxlh8KKOGZkrlQrNOP18F3q$*}COLs#sIbyRLcu z3{VB*7fi~BR@~ycoMqvZ16de=o!TZES=FBjF+a!b7|)!d8i%3!hF;E0_>>Keu0E~p zs)^v&{(c#|_8z;2j>_ti#wywz_uz5i;OC=V#;3;m_T^p1U;D%U`X-yRE3c!c_y8X_A!3+*`k7uUzkvA5Gm}FNh%UH_BN7x*0qG#$Ku?oh{d0Y=RlYiF@54Y1m_fJod%RPTO z=~;tyGBl0AS|*>VV6!hQ9E^8h?YP50X6aisU_i@Y%w){oO!c)?u=ID}TVW8FpbjX6 zM~6_&Y-{Bu^$K=Cu!4XHn<>ABh(08u0fOXwbA;yIv`2Cl>n9W`Q2UBdXGsBKVEshi&62@;(6Fz$Uo9f$jqv~| zYkVhSjV~(h9V=hc#1y~c8g@O)4gDI@F~T+MPpBxA8~Ye+en|_L{WPWYN2NUtGj(P% zKs;rQ)mY#xHsmZm%@m4M;{7oJ(eB_&s$%2BmtY zhRX*6Y2u6w&tFGd$m#-T$3xD@-U8Ri*w~rdVo5-gzw$+2O&;no{H@L^IX;kaVE%Si z+c@M}P_u%Z7;-HugXUZB0aDCBZ^$YtB1Oa9k_!Xay&= zf-_nHZ7q;%4Qcx*2tx$1T0y*4VAcv~i65dPMJt#XB0#lgK@g~RIq0(6^4M_rudu%b z`!Ba;8wvl0`KS0e#%}$@^HkvH%>D$kl7h3F)u%5*q^v-EIS{`fBz}+=gD+eZaxG|d zg0?k&6`DB?s-yuu*`L^V8;KBmTOc;fc1MU;z-97S0p-nsGr*y~3NZPq08EZ5aCSU! zhN$G9LQM+M$wvhukb~l9h#b=?Fe^l%;P9o_@VHhLqH|5~nZ^=ONn{Bj#3rra5v^d0RzUU;q9fEKkP4YZ2+^z+ zgc=5-kYR*GA=?NM9M=lSLP7|#jt~JENQi)JBt#HwA8Z_n1)&{|;O7jV+7E*H`NgpIVR&LL*sq1egY`rO62e)Z29Imz5K_?1 zAzoHUy#HRCcU!K#2$c|v$FjaxC7{Cl_3~g+{9fq$O;@2}K|sZ#kP0jJow+Ud4nxT9YmFB3-%h3bq<$7yXQfmU{&Yf>v;D%OUhNkMqypvuRM;Acy?F)IOQZ z3H8Ys<;W-d&7r@gB6xDhoW~h?<3Mb3#i7F}oNY6NKC@z3_^$34$sC7{Kr;U}UmTLn z@9v0eQ#78rcA{30r4`6pL4j7VKr2|J6_jfQRa(Is2($xDtrlv~3N~p4k7xy3w1RC~ z!HysS-oq1vUikS(p}p{b$|fKjV}akuvcxCDRe?zVO%tN?`WSV?5aaz{GATmWj)H;k z3dC7#;0&%KKFD~O>o98tNm@aQRxnX3$kGaAt)L)C;I_Oy2sS&JZg={39^ICYNH{t~ z*xze9O4+Wwsc?`kTt1XJny62bzb5j^ONSMh`USmJq*9 zLq`Z7YPR6XYy-y795_R2{R|t3eZ+SujkDsIgVXAJ;{<9Z26|(bFOu6*N%=|Bt6A)aqR6_gVMdzsQh>g(|c_CBA- zg)Ej2w`CZmyyrcgngHBO;%LL~SPkb@+QjoDS?5XQ^CSi5Nfw+ZS>&r();Q(oU9UP% z5;V4=uh;teI>l!@4cZ9(xu^3?5L(Fu1O6vyavSQK5VaX(a1k`_3H+fDU4Gw0!@zf& z&w@YW)8)2YZG<&!KN4&WZp(NQJ{%&{?JO{j|7)x7f1Mqmb%R|IQ%t6yahduXPj1V; zaI{JIJHZXr{}h4xY_M7VuiOanH$`wsbh{&e0(O9R(-u^8=+m9-&TQ{35~82x4k3bO zt>A!GKwUH>3iZ+u!Evp?qZORg3aGD!=#YY@ldNiJ${Rw^tk)-STb?k$a9-=UU^wb? zAr}nC_k@h*2E$`sJoWWBo+k@lm~11VJ48q}=Ql4p`9Ho7`yJem)}?Pj-_^=!EYQmO z8VbALb!9nuPpXa&c$0#AqllgX11_?I}fXF@Jx?jr@= z#Afv@c57Xmb+c9t-CfVp-nRc|?y$4W6=;*8hv$7`scI%#LAh2?6(n$5e)tVM==47X zyVc-%+xK&yZp%zk+taR9>tD2D4d+`bA_;SF5&B-Of}9vaW`&&n_ul_vwcP;k7MzFP zM{n(BgZ^d!Q%o*5L~*b4J*^dzvki$Q=4r7Db6yrbT2&{51h@&6vvDrAb1uj8hU`uh zekbt?yAkzMa>7CPqF8ApZLf=hK%4nkEflX6n0twp)Qc=7D5BeP;xtTg`D?+Z=(c=L z!om~TT$Zi8W6GsAPfRAev3^i49`5`F=5wk+#{*N8PG|zQwMlkd_asz%F)A#^f-W_teS=%K;9RNBxIe_4rsU;r}A~>!U z=(z#pfZRYJgT4BI5dC7qQAiXTi$Vm!1ChqSf;O@H9CqB6q%UEpPre%3VYK|juS~JV z(Mh3c|EqfvGi|}y7s}TAtz}$T7aA$)d8sot-Dc~c)tkV zMB@J$1GukDLvzm~={GWx6k3TA4t@3M&2r?Jwu_}j^d{U%*!4jp*YLYq1+abE> zcXyzD35W2Mu$H$e2Z8hCl`r8x3;c!c``KNp#Xu$D?EE^^X0E@- zI8oD7iT=bjo)A^KO1(;9A&M%cr5 zu;M^Y5HBg@%s<@_c9M1X#!3zo{lpuum5!0??v0YV!Vxw-*sON4*88bBB)sZbJzv@u ziN810mWQ=_TOK(RE>~7ZELG-*Emce^XL>#_jK@$`^N{KJNT28iHGm&ws{uq;1N2e} zI6C^Z_I}f@qo!Sl_IqOYcgEY7#6+MXZTmkOV0Wi~-Gv`68@|B%m71ZHZsIF(iTEYt ze_W2&uw)@q^ppBOT9tP_XJcRrONlwC!#}#^fcIXvB5rq3u3J+nugE9jc7OMa zl!-lA`0@-#{MI_e1|0ZdE(#n=#R(~~YJAiS*h>%aH;K>_@N0A~r(Zwg4+GKPx~rKr?>`=I+BGv~|3|R{(MRSF+5d6Wz}IlW zH1Lp`-@e+lHP2*Ns;u_?8k5qvO-*CIKNHSt&QBt5$o}I|1M&AFW|{`>U;j1i>|V-i z;!?acU}WvR(UNm`EsWhcwD#U1k`sRsb|zd3UOndU8!r1@ec>D{{k=H61MV#T4lI60 zfA~|h()<_5HBMxsIh!tE*@D-y(O=TTUrwMGa?sll)JNrEb837NLUB?HtGGaB#XR!dae zIh9|*O9bB7|5^~lhUT?(Pu14(tNOcthaDu-SJwI~)$MFOA4}4QOpeE1!v}eL2 zD7!m-B@7O)=h(RuXZRaw0WYmmu_|?+O8uyJ3Tkrkav;2pdu5+Mq6+M%X;?4mYqj+^16UDkb$n6{%E=O1<}N?vGptq*EX2sN<}>LVIj-0AaGUH9+py^O0=U4I_T+OhkY-YHq7wyRWY zAJmm96``iMr4P!c=IcZFeDs{pU+g`_x0_h*-&U!@K6LF+slzHYwhwBfO1-60QGHO) z>}DxGsZ#U%$k&akt|wG#?~dMcAFonBQ>lmgpuYSmOL3!0RrNu&tJDK3HLVZo*DCcR zl}hY``or@q#d4LZA^Sexo4{yF0Kgx8@HE5curqpFOQh#Dh`NlPiM>blRHzBuV`7ba zQ6JP)mAY8XMmHIRKO5dr5%}{t-cbYCcNF^`#J;21_Yn3S%f2sS-*N2w680U>zDKa{ z1onLy`!=)hE7^A<`yS1{li2q)>^qr#k7eH}?0Y=>PGjE_*!M*CJ&Ap1vhN$&cNY7e z!oG9Z_f+;Rv+rr_+s?jcuTb@Fp!xYM+5N@@2%NJ1y zql>rvE(-0W&_fjZErn==?%YD57byf^<}E)=p+*XQOriTIgqwj<=Nbyp7DwkQ3enr7 zI!h@;FAMFwg+h50DxlDO3QeWZQVL~K$U&j;6sn_85`}(Dq01=rOA5tN=xGXJ#o{fe z_m_6kD}2jeqtK~u5qg(G-4yar=wk|HrqItR^b-m_N1+A^9j4Gd6#9rl4ho&5&<`l&r4ar7+42#Xs&&qz&e%ZQ0RFIo%;tutrYqap&Cb7 z$;u@a%ga_d#+I(FSiV@oeCC9lkpT~bl8xMax+Nfk}4E-SN_)M&*3tpzq> zXP4DDD;!d3MP*glD)Z8k4XG_%UR}1tQCYp# zTvJw6QeA@Jchb1D(z&WM*}T~4FjrMqE-9<2F)yz%mzI^3juq6iylP2JS@j)d)#l~M zvai(NJvOS56~2JVsMZ~nj2*}8=e(+!WVWL|QNb0JOG+xtSC6W>+B|c*KiyvVK;71` z;B4orRj5PMa_RC?^QuY*Q{^Z#mzb;7ODIx}qh!^RGIQlpwQ2P9XIDBaR6V6-jK z2K98|?6QiolA5x@@@kl%omw`(Y$eQFy=<+jvXx3Ttb>+RRV^saGZ&nJx=hejJW z%BeXWH%=Potf?Nic=@VvtXop0sM>6lwN)h*3+I$M@?pHC&XrXxQ?{H*W^;DQl5*-{ zOk!rDQ59(4TdA{BS4K^aswo~Mtu3pOR#l3qL^_UMR1T8tm6a>#g0srJ7zJNZRZ<$L zxEk})>dKX>2T%=ZkTiu;3t8^wS1)&zQ3-NTg1O`ua@Q;=t8y%_TxDCev~oWD%@pVI zic-{_PDJD`yK?19_(ctc3%pv+zd$XLqY?XZLM}`qzeq*%CG5Kh^LCW2B!pYScPPa9 zg$~L)yXZ@WWr0cNI+j<|q@-l1?UY@)%28ceQBhW%lEO{TwyMhMNJ&Z6`s0#Sr6tv+ zW>_K{3($FM;D(o!L$Zo|#^N%wv!<+cvUz36DkmI~|9W{zjk%8>+!pnm8f-ccxKjv%@*@0=OAfnW%Uy3tmjjHH(ukL$qbU1rwOo2 zs!M9h&E=?ibgMzqw31cQ)Uw4=eo3`tt*XXxtu)QK3jZr4t8shvxx^vOsJuhU zDO)0qN*P}|$^V7Zv{E|L{+GsvcF;mLq+z@%S;A&7tg{E4CtNf--_puzJ{0?=R^}Qf z_%b+g*xHiHm1FNHsko!8rgEucbxC#ESoFe@m18lraa2}TNP#I%bs21{+A+yowx){p z||?Bh;H9qY!YgqDEi*q5)MDc_RHRjZH& z)%UWB%EfRk@H5rEsga-#mJf5)@+#j9LQ{QxCB6z3O0M0^CWvDP zo0p+Cs3Q~8k!-H4Hp7FmYfCZnRR^qv{$bjT@;FLhb+9jrzofF7rmj30ObXUkI;*+X z%8JsmYBUT+bEZ5GX<_if_{#+|G2g5xSDw%^_~?90&@ng>!mipqm(LK|ybqQzoiO&nR=OuB=uIFhPwmCwq2o?uw%|<9&2Y z_|-`W&>Q-nAW1I%W2;m8Eu9qL|5^&rN7Z!$LPq?zk|-1Zad%1mcpn`WPCMu9@})T9 zM|t$=KOXWx;4=E64+ZGA4^33G2k>k}l4aH7uV-BzZk#a8qpK>g1}t6Z zurE%@Mb%~a;qMsHm;Utu1<-A?FHfBSy{Q556fBzpIsPZ|oBDn8OA)^g8%B@LND25q z2>)~NpU$aTu>(UN)ULsBg%oBeF@zi5TwpW|9UMWjS&+?wY!+m*q$-1C9wA9P5upPA zABFuU;;!m;gvb3HU-;0qeP9+qc)MAWYH^%#l_YJ!@y0|+YR9n~$8H>pM&Zv=J}F7N zu7e&N_ou;#aXbS{Zp4xD7A;MY;&|J`>Q6meTPx=e&*{9%L? zF>8P{;J^SWEHY9uSR$pc-{Tk-C52Z-Nyfy1k~DUp6mTL02*lmJFGLrN~VP??d=K98chgjD$^!mJIjcxH}pdi;=?D#7Kr}WXdx{3acF| zg`XKJ8M3fd+<1``_6d#`Uo3^=p3K0hmq=j|!=;F~Vb7oZhZOd!5mM9+^tlxYQg{g- zq^Y`GGCYjq*2|>`Q=$}Bj}BHkMhbgvj3iA+mcr|kC4>DsDPql7ioK9*DJ&NoFlTb4u!da8umk&0qw*wUMV=J)i#$oXT$aL? zVPmGmCWSp=lccMrN#Ui_q_BskNm9ymw*HrB+m1elJ7-IVHL-@+=*Xx|*crgK3ftpZ za5Ek-!Y77N__7X({b5*k3?58oAL5V&=|;}YR>c8sN*&u4BCZ1e%Q<&r9~|u~(Z_IS zft}?j^3ms>lB8H)zOb#s@^%~7^D(0(DeBX|su}g=Z(3bs;{dnuM-dOO^pnAL<9`%A ziiOW>Jf6Qi8lIIO_3$|7mumUEx`-b^GwIC%cbs$BYEnPGeB_7bgY?p)Ubo$<=Ho>z zKedkKne=W3_a)~lwA}Q%0ge9BsrhIJAHPV|d$X3$JD)!X{#&j;0b%y(Bj1#tWNgS@ z^d0mUf!|Z4=I3dyzOVWw{gj`KVwGRb`KTK@Re>*GLH4v4`j0MDFQc2NA0Nk0slHEu zTfp_+#O3~c@OC6F8tqxhxd~b>DBU!0cXN)m(AmeAZnkO{q^~H5%ix$l-5`!0ar+V1 zhwVD`(~G|C;M|7y?X7k4qO!H=r#oV`1J8pvWqxypYikyxjbrxCjFrL>;Qie z*LxM`{pIK9i8!7EH-vL~{w>i6-^VGPa@(3c5rBIhPSKl}LFFQ~kw;HGgdN6Q8E z$3}2V1@!F%=W7?b@4`O5bonWj_b9kWxIUjh_1AMyf2Ci|c{q2TzM%XigY)&*|KjQR z^#yT-;C`}@S+jStmJ7tblVeSJzv4bHG(`Zn~D^=hPk*;F>tsS2_BdkNu9vkJd?tZ0rF6t^!<*0M`U= zz5uru++@zxA=X4C{t^a#eEJH`pI2pK{_sex zifvS*1?Hha^QuyC-*E0@E!T_QCh!l@!~FQ{)AG{-^T55}lFL+%9*t)oo&0u#e`A@- z>y1Bg(Df|m^wKYaK3ln}&xA02qS}tD0$)Dt>bTDA5qguBtC!PMJWyZ!oH)80pT;>~ z`}zAp5J&SdpT31U`e;73kn7X4W15d;bM6%4u#c}Ce11atqxsm-3SMrG^_D-HkDXbe za#w4)Apb@4vD2LEt6gZ^qxslT&aH^x3Vr$O#g2}GA6})V-&Z?Qc_N_eNzS>n`3&-7 z$>3sdSM|}{oPB)hbB^*!^Re!GRPI@h`Riv!=(>Q?uLpmkTIK(u<@q^r+rd3vqjKNq za5NutI8<(_mJ2Em&By+WbL5-Y$5$SHO6k&k>=frlbGg4fK^)D;dN`+N-wtq+Q_Y{= z{Ax3}j|KF#fcuPddiuJ-9pc;yopQus-f}U|-+A+lpz>yd8^yV)I{He%Wpi$#4%Y~7 z4(Ig7@txq7a&CrJU(h_`D7ePH(hcf=5tz4Z=G-Q&KF*O{CxeT)Lv1(w$#wPPvzK1h z-9_M~)hbV875n&jIj~Pr53Y%Gd0H+gpWDGb%Q@`ts2`s`J}!{Gc7VHvejtX=Em|%} z-#Kt+?ozo0I$R>=FG=fEu1d@02FhUvSH(GXw+FB$){SdZ?p3Q5 zj{>QmUi8HwZ|R&vRrkr!JZc!{%sL#+qhbWoZKOKkTwi{qzxF!I>p{<-QhR;CxxV%j zs9vePUKQY)!2PEHN513%0qz7irvMj?`npwsO9N*W;AmcWwE$NSE=GXc4(=;%?|Svz z0q$J^?i{$61-L}aah?$1?BHAioCDnL0^DYB^EpQ^eqo!6VSI4+ztVKH1GYH07vuQ`vf?e_pT7&Xx>{Wz|lUW zMS$B1ZnOZ`4$dUN^?>t@_j>J-fO+Cad|sf(<$!xlfU5xatN_;pZnFTl7u*KU4b$l# zC%{#6PS1}=W1d*ZxxqU6(!kvyz!iZT!8yJ4YdyFjoYU(^+rjnR>1ChGb@JB%u8VVe z`p$uSlXH6IO~ia^rvPUM_pm^^4sdsK?h>7HYzDVNKwk^ESpxZ^d10D>zBo)$MhI}3 zG?@|LO2PeQt=evS{iqS#9|gFb;C6FPuYHe#`-OnM2+V8m7vPd+ z{E@eJB9808<#JB199zL%E0AtGxHtiQJ>b6K>mt2$6W}b43vfB$S_QZYaN7mACU8Fy z;P!%B!#Q352d<2B2&$i6<`L0w0(k=Irh&VTb9#QG2p3}o^wonq#n)AO?LluuIx4_* zfO|!NI|pu?0GAkz`=kP#9h^gebAY==fZGf%OMq(uH=1)OQlIVH4Q`--zBp_?e$Mwd z^x7j6+@A!vQgAPFPS^i|+bY2A1m_k=_b9j;0eulDWRU=u3@%rID+D)IfLjM{xB#~m zoFu@tgX`q`S9 zj>Y^yfJ*~6Qh+N0H$Z@^2lqMOkJW3p?ck1ZPOm*Wz_keII|uGb0giqj_mBW*2Y0Uk z=KxpEIWleb>190H3~q*iz7}vd2>p zllBX6JHh>yb9(i66x=2Oee@2&wE|o+xWxioA-L%R+&XY+0^C+`BL%p2a03Lm9&n%Y z`%HT6mVi!nM1acyw_kv(0QXw~t_j>v1-QN7)(LPYz?E@MZ=M*9i?apvrGc9)z!iZ@ z6yWN?#RzcQ!JXpw9rfC+1KcqI?i{!S0$d^%sn2juufN&BJuINl0j`#F$U>ifV>7sw z0$dBYTR5jTzwZW@$vM4wV%%^%!z;jLf(sMiO2Pe^-&fX4w-MX{&gs?jPH;~Na7V#4 z32+g3qrja4`AY^@EWj0l%MwU;9k|f~+*WYW0$e+|FDuo32EF?00e6IRdhL;b-+I5u zIlcat18%E8x)tCy3UE!}ssz&A3+`qC?gY3g0_jGN!2L}DeQDr^3g{~WcbZ9bBt`z7BBP1-NtIej>mn;^y8O&guCpJGe3deGYKb1-Q-NQU%g&0e2ba z^y;r0T%>@$IBeGTtW?`g&!1+3dz*85?OO_NuK?Ew?r{NbC%6XA>Gj{E;8qCei@+b# zm?gj^gS$b1D+D)EfLjMHT!7mOu9H7!rB~1G;QlDU^?-X$fJ?Xx?*|d!a=@(@;3~i^ z6X2S_<#SHYp7(;AAi$jfcewx;jhhNl0$dunFZlCkdig5?_nrV(5AIa~ZacW`0$c~U z2L-ru;O-IN63u8g0nQGtK!9_A%Mjo;gS%3IYXLV{fa?a=^8?kd>9t$j6&Q~MxJ+=Z z0$eG$r#XkEi2CVez26A#Q2~8B!PN_JN5QQW;3BS+qj$`+bN(g;VP_W1h^bng}Q|dH-j#;mL)4+Woz!iaeRe-Ap_n!jXc5n{}a2?>B0^B)p zw+e8Hqwu_s0A~kxwE*V;7bC!J2KNs;TEM+4z;%OrS%8Zhjpuy?xJ+;^0j?C> z?VQu|yN%#(;hdg5?*x~}IlX><6kM`E{vxhM{}AAk!JS#Ejvspcw-DU>0^B-q`#7i9 zzFWa<70}lX?tTG%J>XUfa0y9x4q1T90hb}bRe-ykb9(jH1TIWK-(GMY{<{yo`a1#c z4bJJ=RrDCF_c^Dx?nwjpm_WKk;OYdpdT`4Hxb5I(3UD3Z(gnD4;4T&560gDU!UZ@x zxKH`-b@bZ90qze1+-7jk3UDppe#SYt^gib=-QXVNoSq%TU5n@8IH%`dGQlkq$X_YA zd;zWz-1P$7PH_Jrz#RqWU84Fcz4nMm#{Q%LmkjQ80j?0-GXmT?a1RS`TfyBez_o*0 zBEaE&4w6lPOSlg2#1!Cizzq}ND!`rPzpvA4k0x**aZYc%+zakC0qz92X9d!Y9*ce? zpf3&FeF9t&xD^8F)`OcRz-CPIlK$6?c5uf8I0v`` zoExI!uQr2wN+_09OGn zmUDXhAWh)TE$qMD_JaGf0Cxi18vaGYa0xfyxogf1(ZpfB5RNx=xUJw;3vlh=ZsDAs zJ@3E4 zxBqc)Gq^W7r)T#q;9lgMUb@}jekH)gGh8aa9_^pzn+`G{Yik^3vM^(^!mpMaF26NuU?|_@LZ_?mj-T? zK)OZX76@?l;HC(0+rcFZa2?<-5#Y{&lLWX#8T%2l`yXfQ;0|$4uRR>#_6TsB!EF^t zw*}lr0j?Wd73cKqInIW4E$8&c!Ax*d1-Mdh;{>=yaPgeev(ug6zMa*7dmIIKLV$~y zhWm(|(<^T>xMu~pLU5Y}(p?AcJ^^kkxN-rm9o!6o{PlplL4Zq`4*w^RZVtEsoYS+b z3UFT)sO_f5HGw67u@eTr)MuG!2M2ui?(At65!Intrp;lz!eK{_29Avxb5J^ z2yh+XOak0FaDSiKe?Lmh$MakQoE_YL0nP#LHv-&daBcyv1ze2)*A4Dw0WNNaB;CY0 zJ%5l1E{St`_FM{Xn1H@UaNo{Q+e0tio!~wd;EsYjB)~5$4&{qiV zM*`eBaH~0|SAScqkC%_ef%M{@1!Cfi9Z3h=6z;%H8%-(-{oCDV( zz$MPce2sHMb?g9tI7a$^0-OU}qX4%VoKt{n0e2hc614dXeqSfJ*#i3F=HPi10WK3< zq5xM4E=GWB1b1q>fd2z`On^HI?tlOnQHc8$0$eh;OYdpb>J!lxUJyk3UKY< zZWQ2pz+ElCCCtTh51iBU&pF`!F->hZJ^x$*?ilCv{BskyR|L4d;GW=|o_{_8&LzM_ z&%=JK0G9@Cz5rJQ&LY6ogS$$A+YWA^0M`NTbDMzw1NSEZE^$8g*915_xL*ly4siDi zaGSxc6yRFG&EcG$f9?jCDZs@o!0(C#xJ+rU`KE;I0?odcY+Ja0$0y|5Sj>0ry#6 z|KnB#xOW7&CUDK18>Su4`FSFad%-;^z?}g1bI!%+=!?D;?{DIqomI(8p%8=hws;F7^L3UGzs z)(CLxz?BMcTfr3waP8nS1-Kq?Nu1LgXA&0TJtqQO4!Cb}`fuL~aGwZpP2k!&r`K+K z!R;2%cLLn61i0u$SSJf`Y2a!ExFT>x0$e?~Tmf!7xUm9U2e{z^+&OTP0GC*T_itqP z-@bNmhXptXxEBSu&EU2Qa4q260$ew^J2My~z-M(02~pH3D4XQp|HWr`H~Ka9?Nj-`^bIJ{I6MgL_?o zYXP@Yfa?bLm;e{I41dR0fXf6|$vM6DCgPoe}78`_crJB%3BETRnF<{bFBmSYXNR6IE8b1_0kTmQh@6LH&=j5SdM*Q0WJsJ z6#`rZxJUu63Ebaq^sT65!5(i{_l3JtwY!U$dy~p_e~9xQ_)m2e{V-xXs{p3UDpp9^;(eIMWSI z5zrS`f!`wvaGBt472rz2)+# zxK}u**Uz_t+a|!ZgZq&{x;@}l3vdZF=tlzS=75_jpsxblI03E+Ts-IW{O(?GMge^% zzQ);6CQuP#r(s0`7Ir4c6hh!969AZrmLhj|8|(aCdTU zm`=K-;FfW2kPg=f&c->ta_j_`EResW;4Tv2B39#fkQ4fEk7RHk3UGzsS~#ayFYCZP z!#TZr*$VEb0_nDcTPwizfLkcQC9J`|ya1O2?pgt^0^Cpmt_j@fbhSP7>TfT&69U`` zaBp%>um47`m84yq)AK`V;5G|zMd0oe;OfDZ3vk=P&EVWcI{l~v+(ZF==fGViz$M;^ zzxOV{*}?rat^a=H0Cz}$+YD}(0M`O;vjEo(?mhu7?k=>40GA1Fh5%OzE{$_~e!LOf zNC9pqxB&v(QE;DMueL{=j=e;z!#+Ca^zxSsu7z`Y{i6`vZw2(N1NTz_eOtk;70}lX zZm|H@18%wimvA@QO@PY*H&TGB05?E@YXbNA`2PFRUT}XB;7)*hk#l5U7C_u@U8scL)Z%}eayKIWXBJ_ooq z&gu1|ÐooZdLt0`BJm>2`y=mvfis_=C7wJa;INzf5p*1h`Ug83J4*xXT5&o#2cD z+);2{DgF1Oi1nBs2yn^Zo)_Q>!96CxtpiubIX%1D3T~-@zIJeS0ewB-#tG<4xDUVE z;arl$KfUbR<^*xq=x`O_hI4(F>2OWpVmLQehuaJ8V$P-La3{cB%DF@xF1jv|KfQjL z2JRo@RJ*!DM_&=RQ=HS&R}bze=k)yZc5uHJ(ANR(cbprplfQG|elC!1Vm<7Xa~JFA zvx8eDpw9trKIioO+GcP$oYSkn7I4=HaNXcy1-Q5kXt%Ly`|8cJGr=9_oL>G)!L@Qu zub(&mfA-!xFsf>M|K5`XL_q{Eid|7u?1+e62?-@i2_zJ;#330%B+P`F1c>65-aDc9 zUIYwXkX{7oRjC5f1q6{M2)xf)`#H%ZdP9DHzu!^T*3i50!maz~@z6Vx54{c0TbB>L3((7? zUS9R@q0Z>Xsh3x~jD%jNeCQ=Z&zFz*Wk9cPKI|=m-n;qGI|99@^PyLy3))9M^vXl; zujg&K<&`fVL+@AW`jB-%zWtWfZou2==}}7_W95&-4)L%VR}p1XB>E71-2BkDbryT1>5p6+evZJ}OX^&kOy z3-Y1Y1A1ermsh)-1HIn)u(uC-t*Dn*zJzthdT~DNJrBLA`Ou4nURmnpm5$cXdzgB8 z%@@W)FNAt|rDFs1PCR?-^1T4PP5ID!s0Yp;g8o`8uYeO z?~Xj|?SS6weCYiRy#ds_Jr8@Odtn}*554NptDg_OROr2)553{gyPtYk%CaA4{!z7k zSg%l=gTvKT&`YKsmUjN9-f8G9qu$fG^~hqcL~pzgk9rT}q4zfQW>ODZiuU8gk0|yM zpm&XWeR$nCdO_*v0lj$Et1`Lug8JP#L3$77)+0-}`=D2k_VTKKVTe?feCRz7y$aNO zHIHy(p%+2DC-Ts14ZU}%S1}L0@z8V9@l+mq8=x0Ud&&Pp?|EFm0KLJ~tD0M{YWspc z3iJx=jk+Zt$;Z-t@mv%2%js1Jr~D$a>tS{1Eudar{eCL+CQ`3hZhK@2cR2KVQLjiI zdaI!4r(Rz3z0=ThQ?Ga)_Db}_xsZI=dmDOh(z6h z_agP4&25jY^&fg=^IK)Qilm7nI)C(Cd?r_>G5N zYwG1$|DpE<_3qCdE?M&B0`!_v@3}nm9?HOco_gWA^{TYjRwDKyq1Tyu<#Ouj^5rl3w70zUIzTUu;r?UX5oB)~^cv(tZwK^hQ!lS@|At6gK=0vv=oJ}?b@F`Zm51K_ z`Oy0qdL^m%R_^{Zs9);3>za6~(3%{NCk7j=44%c!2 zjP9AV@5IyL{}tXBq`dhgFaSQj77?b7odFK}H@wagAdJFe&Z{fc47Vg(>;a&=3 zu)IE1Z{gnT7ViCT;lAJ&?gwt+UI1gIygtv}!oBt_+*{tleZ(!?SKh+?ioQt2C(O394HM}jG<-Uf5xZ_gN{GPZLN&eI{cd}{I zrl6RMOKjCDF2&;y_`L4qq}24d7G+JF$$vS<``j%$fmC;V)42F1P2&>N-2Mck9s{Wf z9=|_Dppnm)8c6lKQ{qy5{#3cr%pKn(F4Y}q8W->+H;%)lg!JSD7)omt*C@#gpK7F` zCZ=}tinmIFluYy2wPa#a-$?cNnD$?Xu|JT1xZ5mPC-ocx|-k2|1Ftt5+T##HsBhWiuaMY)YD z_>#u4i(abhZHctU)$s(_TYlPD&Ps z$>QKn^(6^p3S<{Vvd`BvE)At)ntvxQ3CZ!vJ{#d>mQi>dPh1$V{4lytGwCKREjq&D$;+zD~b(ojF*ko2bF)a9Q}XJ6JG~ zmD!^ez1hV!SuX_Q-CixlOvO}xvaKE85xw!K{*33ctZO!+Yn9UjsUEFDTLjYr$sUiU zYL{S(Kz4}66=w>V(3n(@yP2tgc2v#&xiTa%E=B*0NKW#2Q>(h0d15^N7U(^5_IjG5Psc8k>pK^Ohc|uDv0WW5WJpLcY;4IHO};1>`Fwy_A*qP$;ClST_zOvHo2Olr6%}V zdQJB*jsDiVOlYh>Jwm#m2$XaqbesXxDZ~jN!q==Bl47P8tb}50n&M6fPf3Z2G_wna zn$hKPri*Ex5~2x@@}kuzCr7(eo0$C}CL}s3#bXwP)J1H}l8{i-zM%;6Yw64^KVB%*EKh>w&E$}9ug{y_%$F9>AjXBp zxEp)Sk?QuRN^%<4^u&7t0k=QhbPCgcG9u0I zN3vtJ?$+=$^TBCQSYlEDMJ7hfh_FzzDqzCQ`1UAS^IK<;KXt*vVTfd*`8G^C&E!;b zB`nPRt=GMN^!8Z>4u%ChPL@Xa-GL_6+}?y_WYdCzG;!WBaRVkYyGj5HRsIfXLcF*s zMkWPPq~zkv_XY4(!`&3Q^?@h7o;x|s6YWlt3K8K$nfTMhr3C5H9f7e9S0OdWfoPYatxG5beAX|RynMP z=7-AhJ>~CxDo^)tY8(i?^^x{R4Q~kJ#GtX02;&jg-nRLX)9^$j`x^OxB1Xw zTBWM{!rQ{kj(!p^Dfn!qIYE}PuO2ECW`^~bXe`WtA5HS*+r*I8m&Ju*oMy4I5OT)J zgMj(5P^B18Jcc1=b0HJTx<0Ist?Q9yXQ8O*2+@x4rJ<4RMWnS}e^W@S<4gp*Wvr%N z@ykBEE-{U-d@+tEMCQr?j#`)XD*klVcNfF^UCym zE86WhKfGH9)f0L6LpZ9kITBmbliJefmx^F|d=wLpV)X`^V7L=!Zp2lKHGd>UdCfUr zy`%(>FV1Wo?uxA!XBLfcnGo%>;>?-bk)XHnwxntXY3YbzeMPdKL~d>m{Wj zw5|E7YHbTUMH1CG932Y!kwAnD2olV^qG4tye4(zX{6pnUwW9{i%)-H?GPS6=v#uG8 zU&dQlggI%(-dV%UxKlFh^qoN&cJoeVX2+sIIcG)|MaC^Eik8HTF*}PQ7u(&XxoC#o zB?U1WJ*jka(~6`f;Vl`H@2nbO_9Ci1r?^}(scsA^4&G@(^p;?LD&p~a{7LcIHFOW$ z^iFZCh})0R;CT2PmgVypTs7OGqAQ`@NZAg*v%cFeonsB@xe`6qFn%(b@HJ(MWX*h) zt0564x`t-jHJDX*rUjDX&2i{$7wdZO2$&srhM7a^BAnM3e2 z1ee0hBz1^M_4yI78C*0v*`03I-xX%QQ|BL5?ooM46&81>aD{E2q<=4y~CE3b;ezJzy^nw)d zaHpk>)t0s_xTTG67*t;{B`N!gxgxD?O;VJZ?aQt>;`Evs-qJQdKsmEsn;`yt*~}Z^ zio_5x5gl_iAMO+h;Ry+T6mOhafboOYu8iVRyTr^JX6utxBkZWUTBTd?zy1SC;R6=^jBy`M@2Z+2h z=_H@m%qxLrP$DMPTrUyniT5>2k;v8aC82Y^d{1R>LU>wglNekQ-QM>`xV`b7WVsfb z6yFrK2iz7C7wQ@C1xtQj-E2p4oD59Z&N#4{F>`_nJFwpQ{~7 z8w8CZ7e*hMCFFwSRTW>d%*`&B2sg9tu~FHNsKoozQzZXVQW$nSEze5sghWquPYX}7 z=~7Z^t;eq>?WJBNYqu*W88oU*Ok^>S?ag>qePhb zLA~x!`KKDdphXi}2Swo5zF^`#zLZ>-rri@}+Rl)$dqASSU<&4LlBP*eN->9In_c{m+o@m0~`|R0InnKYg^MNYn~C7rh=bh}SVotNbrv;F~Xay8K%cxCK){9z0$<&S+2NU!BF zhaU_x*Cne%QqA8Fcr{1GK8=~_rA=70S@B?$S6YwO<(L#qOQeV=Jys_vzDYE?BoF!> z^Yi190Uj?p8JmniR*VK@#Yh9+|DePwL6TqwlnOJQXG(n-0Et_&47^Ochcr_$^w(kD zq$kbo5kB+lLzU8_bduWP;X0<BoS&|2RAWfJ}bjBl5kxo>H zKBA^ZKZ2Q2Bc$3)d?d__R4-Fh&KHj%*5mv~v?eZw%4&7ouJVYegrd@$^N;u>owk(B zx+k=}#jX?Fq|$g*x*7kd!{0xu!7qO_)}3h9*w&TZT+;PCOQ+)*_SK$-YE0tdt$uNIi-5C3;euh*b%W3hSS= zL7DcY>U$bR%SfwhDeVnSZ`9IEwY^;BHkF4}UQpR_u7o^La%MSB_S;1eNSN4P}Fd(h|d3^UdSdo0<7| znAxw_ZB!occFWtV=(YHr}7pT2;D)*{9t@66cehVbbaVqDiT&?mamB&?HQQ38& z#!lsANKQabKPeLs8DYL`I=^LZt811$g<*+~IbS!w1dn=8H>?&L8!f7@m{XWna!_tB z^cvujQRpkC=PWz_u_iAfq>$_}>@FFM>KJz9f5XgTiAF)W%}#nc#$0+P%5;6M67QHb zJ)VSs?cP((TWNV*wNB<<8r(YbMBZVxtX*tBXhGl zSPIo@@mm>`QA);(ry87z`v7UJ>q!sIexW}9Z~K1olg9YA{$>Uxu41#8+z%EVNz z6BQjT%lO-#u}e*r+^MjT9_?$17PRY`FmqfjoEHlr(dK%YW)iP?&!EbhA80tntRAVhn-GpXiJcGm)rhUjWk)jMmyehxJ9oV(<%LX01jw zU`9QMxk^yEFM2^2T5bAe^onVKIr$tTjS6UI-kTG^ZE-hc8x$2Tq|OXlcH7&d9<{GjQ~VWW3hPEFOn-WIo)4Kq7bvzf1j zCx`a@vU=0C!7hg0ud1isHkQ>`ZYm#U&dJq+sQIY&wj!G)FV{4yUbYaEjKw<)*6sYw zTzDCazNs>`rta03ZFSTX@LHR4(k0|hRhexER*SQ;$_@-b={a3 zet$X4bj7v-ZeS`WsGO~GxysFurl50|{jr=4$($*tN$d5B-0aZhqCZ+&Uiu7Hj6LOR z=`i66(!O5}ol^Oi%FfHQBCDJTiMb?4*|^jgU)9zwbFuvTmv@wh zl*e7Bx>@pyH$n4XXA#&8TY>9Z$9BAu}^e3ncS=Ze z6VNaI@~U)go`hH*ui;&kVW!tgiNi3JQy@(UYGzg-G(Dt(%$oe!ZQHD_5Nvuuh1{l> zRzQKN(N$^`jbA587+Y(TI9$PEz?_8UX4T1UqxWmM6Epxl(mu6%Tz#N3ne$q1Z$BYR zoHTEFEoYAGgMssE;HJuMvQ=jWK$?OuHsN)2RZR2QH|(00>H20A60UY+>bdOAXtV82 zt7N;lYj2uRJL3M~b=^>Mmb6a3p}x$dH%$Ru{l&(e^{9C;?2YQ-HG_D}8&%AtH#kcR zy1xGn%zNW8XZHDnt{#3P%uLnj&zI=i*=rz3Ve1?05rZ%O3}36&*LIbMRGw9N9TIgI zn~Soh-TqD6P5$sEZgZ=}rFI&|EM^STZmI2CSSJp0T}W560p2FTAT|V!RN#1SY=Gt2^X8hY&q)blo z+V20aw^5ik-^MDXbVX+VJF>!xK~&=;e>3fzWC&wAylYFC#l;GVH3aq4?OhX+p#Edf zSz@uljg{Jf8Tod6yeCDsd(EG3V@9IU@|Y!Wr!_)PWsRWy=$HW}#H(X++uN!59SsvA zs}J@@as2HVU$W1ffuTj3%5^GtsXVNvajU=@rZWQcd7ZqP`Ht4PT(Q3K4(3|w&&JxU ze#Z=a7rT;rO%f(q!L*!z=bvV;z(D6U+A36zQ#n)RQkCme?t+xDy+`K?<;=2ou>hC7 z4sXZ*e~fftsUcg_9L=Rw2`i_UN4o4L-Ng5#%SGI<6g#(KgjrLumKQmR-c6^ePPdj| zI)q2c%9Jm?lE)p7LE6A@6N>wMxY_<5uXL!GbrZE$WJ5)Ul>xK7B1ZdIxARzsfVuIW zbHB~4`h7**KPC2Ax1}i+o$UX%qGM7!{o&zZrsG=4w*D%|sQgytVn`E;v2H5EG&d^h za(PhVOtuUQrm)5Pt^ z93`T1E8N!t=3=BQs^L%EtFA?2;p#_MNO&OK8*fHL*r|e21GPT^ENfxA|uQ$M1GCNWv9kPPduHB z)S6(?naW}$i9;l3E8BkkT4h;S3->0dM%yYF1Ryl*%&Aay;71>lZ%uL(h za$$tP#%iuZt!hN1nNT%2&!ce8r5<*{ou)V&TC^4I_a*wVDP>lmI_qt0Evzoq=xtJY zKqAQz!8&7*f|&?B%&gETB&DKaos4p0+w@u#Hi^~g zh{}s9J8jZ(g)|`&u9M?a-ghGCT$u#ZlT~|xYD;R&y7#jZY1X5VvPf(N%%S&V%o*gz zI`y|#{W;Y|^DJNnd?0;9LU4Itix**?RzKHO_S!759HlZ-P}uqI(VLCO6lJF-L3GO)=AJMPR@OTTSF1t!Jlt zM`N2nRzN&KSF@_+=4d$zV|vzcDnI7C*wWN>6q!BA)^(Sd@KhXWZ4%@p4f~rIXI*z( z4XlgcoP?|d1x)8USZTD&O7_B!H6(rZ@66ivY=)U1gVQOJibCRG*3=A&o%y9!O|vsv zi)>!)pkU3?taxJblryI3W>icK3?*nMA;_LAEt`WeHe?O%3gV6d@GIra?wVDwdxmAv znpg|bygebYl8*UGP1*gk@m?G)cQxqst`%h1bj4+BXG|??XHu;&b4l%V*eZ6U-wAJ# zD7%>Ie^)KptP6^J4E9km&_{Okg9kLmt*)?eGax!j1`;cy<&YI_pC|#`A+XG6evH=r zhpDwOnNtsoG#oD$_>ytHQI`nkN81XJfEyS#yHm|+m|v&byHp-l`G?Azkl6BRX0AtL z=`yb@Jce8gN`1hLuZ{KRT8&xSa+{a^n3)Ky=Qc^pV3iXgnR^>+2l+RrYCE}iptjAu zqe#_!^|V&y?rpfY=15xGIymp9Zr9YTbBZe{N6qXy!L$HA`fQizjDa*^ntkW$=-Pn2 z^IWPEgT>Gq*ah(=n7`{V%XQY>4r!UCo>!{eq4KE8OWUz6=JQK64VWQN?7Tx0t#XRW zg(}xW=Eyk~3x-GmvnR&Zu=6mmTh))NyrQz}PBA$Y(#Vm`3gyj!*hnAxagP}m>+GM6 zi48N~sNy0~lo|=eIga8&IB!(vU(}h4>APZO(r3qIhk9aMc2;#d{3IsixR-U8{^fX2 z7nWMg(z*iM>+zgrWOW=7Yk+lSd+bthZv)d2EAXs^j;krKFmXA0l>0 zvlxQDj8vU9lg$mV43*W}n(6hb`*2Xjhl8|O0$*M)A{|vNUd~spsaG#NqLLfuLa@(X zFESbDm;!R_6L_UQ7RIx2>xbwyf5M;*V}tYcWe2m0>CgcC6G$RjkIDs-+}l`#%{TSK z&Bpqb-F_TL!>j~5q)l*e7fEcT?dnGTNHe)XRsfNn6s(S!@gLg7P_zHT;CoWPh8Qbg zM3c3wzovdAi~+Rz4MxmXX+}Fk{b0?2Ib9!7o(iSS8p%B>PpG`4-f*z4zQ@y4_J_wd zz?yY(x(r@rIbdYNFw@~@DZ}a!wtD~AAUWRY-m^OXHc;ygHB>H@lU2@Bk0~io^EIy3 z*vSCd+v@ir4#NH8a=`R}p;hX6o63VKFNz`5SS^{zB)?hzpTg_8X46s^HzILZmFED>>{VzDT@H+4Lz)XRjpC>W)wUacK`yv|O$Hp|bs6DRb&y{WOQsHV82j z?(m^T&(N`f0OI{P%^FPFe;fazL5~-0UhZbjPaJ zY?Z52?o@dU(iGH5T=scCv+Xn4$Jq5*tm);JW0=`a0W-smVRzQ#WsW;45acS#=ra$v zF=-OZA=z%SCfdj^C(^sP>(%+U>qp_M%YO0YPr^Agf3}X9?8d=}g>WATca)OK*{Z7} zy6x^9!)~)1s>@Yrv&#J{PpAibcWU3ro>s85EFL2)>uQELF6M`L6DEr{^BYOS+TAGH zTx*2H*YhP~tHI2PH#p#B4}NWIBm))cDz`VnO5!gP$pMgPfVeK_=cPKPVgBEUz850I z(W)WMpj*5<4cn+=aq*j9Tp>CO!Xp4?a=aW;H#2a3o*G@Qa)ZjBRUTD&4pIm2lM^Db zBW#c5>yBUz*~;RX)6EH$%+3TP!SwRjsGFml)|qwAkV^+KW=ta1KGQs=e-lYTGTM&m zm1IKxhv6S(tJBPBg2s42(%uhJ?#jFe2FIZt<_uVw(uJy+G;|i2$ae8yb3r{$7LQ>Z zE$MA-fhXE5^=M>l%=R~N?yQFq-a-wjW>p#5##Jca3WJmb(z8g6t^V93?5(O_BbfX z9jP)C(iF&5;9V25g6o~g;07eJ5)4aZQfy3rO2nj9O>I&6tI7*vHncvTzOs{Dvo0|z zX!~qSVq{VRU3wxz+8vS@^?@{@dLZChV&!Ca3eK71+`k+EFm17Uf^GY7tPXdEhY9Lo zp2{^UcZp{#;AVw5qe*rcU67r}R3WQOWJ33IRy~ol$dzxQctP#8`&H~^si&KENlvA* z3I86d{h@?Aq^br5JtZkS=;ctGrE1GmZc=$biY@K=6>~Hx>pZqxc-8#W%N1sB zsAm7e)HIzx&(<>gdSN^oVedUZeU2U28=psd8sl^?4um*s9%nIH(Zhf^cMyF2Vlp}i zxnQp0`H^IOE^(X}j}is>z4CbkI>#hgs#^(F*sZ%)qXdpbtF z;@kB0VPxqugOhp4UmFunOvqT$hm#A7ljY!n$8LO@l@hU9ew;V3%i-8C^P3-&>zJ)x z={Lg@JzhM0f}6%LUwESc`n6OK4Po{_$%kihK!OZdG|e z<(b1e$FeJ$SOM0}a&s~nw*__YdPMvUg2dSXbfwL#COS|@xsop-T?ZB;yt&7j(RlJ_ zJ#v>;w>4`nXRGVG0}}OwTF8Xyh`G%MbEKJAGHs!+H~EXrY-+AGL(SJ%%$8VK$G%<- za*!8y4|UtDa=&T>7td-Op3a#YU6C87RQoU0cDIyF*RZd9^`x#RP>)A>Bjjwjo*?m< zG4R*N!_ANWNOL(*)m)Ra zHPFR$+5C`J1BY_3ZzMy}b7^`MMaDjG-Kv%2$N%eJ@X$H1%djr@IVev4CZb)`L z8L{fh^V$~G+*pH-k4{U*IBf?i6HaNPb_8GDgD}pk_4X&VXjD#6IZx#pmAh1)RC!fp z_uoW+xXS4&m#EyV@*pG|=kbUXhI2W~&#<`-Eq#Glu?D}P<6QJrn7OI(>vl@w7h!s(W1%BGGDn@d_C;0m z^OxBTb!_XPhPt8kzckbp2op_mQhM$tIsc1)Ym#fyohR7VB0>WsDzfuw4yOmV!6{#= z(HrUiY=akJHoFau`@$A~t~S^SM>)sAHntW{Ic;$97kG3-uWfWf4QhiYzxZbxJPRLt z)yGMdm&KpcS_XY-Z+BlhZEM_@7)_s+F#D?<1DU10+G?-Q(%uT~Z&h!x%Jrh1)wXVY z`9Is%1>|>*b~C(nP}^#Qgzr;RIdql<>s&>~pV2^XsO)`4g0^kzeCvN}TRr|O3CV6x zf;fZC6<=eB3iIjPLA9Gn2a`P;Y`p zBq)e}ZCt5lPzQ_?PKu9$Hlml?xHP6Gy1Qi9=fy?LX-5wi z7EH0WiIpoXzRL7qr{sg|`3!37s*>anq~dSqL2r2s;y^o>DTL9WoH1$J&c%&Ed1k6} zJFKta1J`y9NC?2ew54>{lDAiNP54{2%+yEE|)#3-(3oRECLKOvnAL*zi z>s0Plc^XnqS*4nlon5{ZCzi4DfTb)tjg{h+#%H(a&GF7zDaq0qF55BJ#n5fBW_1@- zmq6CO`o1nM>ocpUf9;Z;jVHReg0|g%?}Ae(x&dRZc5#}eY10)k(5-g7GD7y+FLXh0 zy)H=FMybqHxm@Kom4{VcP}%XKXbyxlq2XpqS6f$p?iyUHS4t|AlO>%&%@+^tb#(>x zEjVl~DV?gOa_DvJmKEKx-Sy!!=WuhtEb0M2-{iWLzISW9u9Z|6w$ zyzg$|w(Zlj>F=t6wP3t1q`y88wyQs?h^HkV*UQLoMdKwWc(T>k3@_qIu}`av&cHp@ zOw7Pz&mJ76#N**u=ftsOAI`j<%W%CPV;{-O>Yx73K>XKBY(t&*hK%py!Vu@LJ}%4@ z%#FTq|4H2Kxu8mZUrP)G%%(o*j_~+hcF04;(jnE9dzhnmrjJWIzJdMVd=}2=i$tlA zF3Mc#>#FASCw(DHW649698$HI884D>>}Aht^~Akh;8 zJv^)+Ph*>3GF+9=GbZB6RBwVlU~(kG6&W2RI|3 z55aY;m8h%F%ZUX93O-SH6)YX12 zxpkto`Ek5m@A&4DgtS$)4~RBerj?e_ak{2fy8f*-P~|s}CRAFnIoBWEySdaKcSAfM{kP~ZQk|_Te^vRX$`04X z!T^<1R4!Dx9#Y1O=7$VdG9F5CC*-O@?PP6fpXy#v+3|*08mMx)%55r-sl2SR%T3W4 zt};{Qa+NMUl{*5XQB z#qdkVp_d2B0LWe+IN08!h7Wex*DEA@vTS7sDx;RU!(=F`zGUALOUT&tFeiuM1sL2F z8a&9_8ac=%YnTB&!?<`5a`cqs1RmU-ISBVIX~Q@+JYO8HXhwBv6M_uPfHWcU_@Ira z?)zYM>%<@zl8wD83ELbT0&ingRgM)iNqq*puqugm@dcg+(?l*7y-YQ{M6^Q#X5nBI zfs>DB)lkF*X)qU&gbrf0r^uKVYS6f8|sqsvboTCI0D(D(LAB@s>&{HB}WFf4RPT} zh@@n^UYMnF86?J0ro&K|PEVa6%q*!B-T;>S!B0Q_s$OZl;cKIZM29Q_d85jr0~ zJ_3zhr%|KjfPUAGqM8*MS-sWumZ2lH$>F}>(RZcz&F+!#n1~L;92JiP|A)uz|L}Mq z%i}5WI36A`>{7W>)Mqjc!8WR$B4YCE2dG{;6s)!-iU ztWoIHu>X(k%26s>LSq{&UX3uDh9PPVn4vt1F^~Cg!(Jp=GmN_OC52>wF zD*sZ8x}kexwA?P6@h;p!&b@T*EO8tFsTGHz;`WBjL9ZnHQc@o4#$ZC}O~_uz&-t1G zvqfx;Q(H4tE>*ch>|*m^)mT?}ygb)}`HS-cpwZ)D(rg>A?a!h1u;x#Y-9 z+wn;ENlB$V!{O`+nQPXy^LR{L{?(@WaSA+M(oE{mMT($bmk?J(S|Am#@{otnWwd1a zOoh^DI2)WO&&s$T#=CMvX@kUJh8kI>x~UQ7_;_^V&N~^tog}p+%YTOnxbI}`f{jIj z^feUPCMQjBX-ye7DJWyQN?bRIX%yw&iLTmd82n^Mh%7%5>X3IY*Q+ zknqdlg99jKeX$NY{8N)%IZK;fFaM8HF`K8jq$|N#R9;*Z^z@n!`y-~Clv8$^Z%zzK zvoi=Bo+IfaCfTGzP?8`8-xlWP3MLC%Y2N0Mzo}IZ?_5S9wI`MU|a;h{-`JC#zfl z8C00Tu|O+{Ge@VoayA_FA}x^ZvVk6FdVGU8NatnSm1*-0dKr6Apwl;q%{mR^XAL8( zw45rk;2ZXPpWD}#(Z*8o_^Wz6tMa;dzfG3tOr@D@(fUQ9DH>(GE6yID3(yS*Q# zxhmuKjQ6B?qLCCCftroeG4gId15+!!-a)S7)?<(C*b&0=85oTZ`4$oA*Hdz86r?;| ziuE&w!Im?2y4Rk3NQDOEi>?)IexD)hNsW^d?Y?~1nRYce_}Yk> z2xqQ@fR|BbN5!m}9yDC;FOgZPN;neR5-*9w>$#j*;{m+sy5gg8k7eSS#QN}!W5Y9G-I3RL!2G?fk9wN~Wj8f3`X=QMo~tFpQe>txN8-%vTtXGUH~Vzl`)W zN=w9X2khln!P}Vi7J2epmwhB5IRDOmD_LwtWg>*1#cHU1zMy9&M|J%(aWmJGzC%Rk zsD|IOw?uV5B!@M{Lek`E#ET302VH^laQgXCI9U(GH^6uq=(ycc#R#^gFYk26P*4Ix>jgx5c5b!Ae15d*W; zz&bUMrPy{>4#m#1ptx5Re^+@^)$v9_=RGgx+CrBpf`P*J*{IElwaoW(F&;WTSDR1f zJQvQMV3-m#wZ~X&{Cu>avER8WjGf{=*>j@93-O>}z!xtsHVcw2Ff0z z6twdF60+ZyAfg&A9HUq?FUzKB3&W1E0IbQQQW}!>Y^k!9U9Bq{NtWy1* zDvznWtg_2Mu{luXc$LdkZdLiK%F8Od3{p*%lT>~OX}9*!c}Tw5IAk*-(A;cq-Ek4`0C>j_ zR#P-qCsbZi?~KNrMOX#)>ucj|6mNTTyco|^tX!;p?SaLpqgbL_BAp&)D5mdH*zP(+l8Tv{*}d2m zsE$E>ES3aKcX1mdZlSexzx~Eyxeq!ubHfrB8s(o$kP@x`LtxZS33vnPDE?<=`&TWz zLnqsYUOA(7{#pX%?=>QN^>}V;H$`DJ6flcX`LoJ{Do?6BGE`g-4injVxWqd<7Zxp* z4A7>zVyR?;{<&6@2vr;s86k4qNXZJ`DvvGAzE#d&F2`G?wwOuFuqkK$8$;yMz)_Nj^(uc?Ib*b3*`adT82P*N zSdj}Lodo@{ToQD7xz>%bE3~GZU6GxVNvm?CWcn(Ev`PG9%z~R1e&b^JilCV6UYV7e zF{1j5s1|TmI}WdK$@v(Wxwpg%eleZ+ekCfBR2{QsWssTeD-jW_B;zo`ft4`x+t?5n zmeRgmiPZqd*tnIsjV;eIwlUlPRq@|;ocf0HyT7{Dxf;)KEpjCf z5Bb*B^1TXW-gvsyL*9lB?^x%$uV`%PqD3AmeaAz0JQS<)&B!;awW>DD_10VUmxr~g zR=s`^eA>G*3oLPMD{$u$SMQMW55*O^RN$_s?>tp#iL1M7TG%nyRafKWr^??^{*I<^ zR(bucny>e9O$%A%syfQmaFJ`7tDEar*Bn=alr|xKTqzBT6uBAl+-TQ>g&uqGtvib5 z{1nHhDEF1t1^s0qycQsKW%aH!%n9dad zm5LU>N5VolMI|ti2tp&IVck=-=sm>|8m@~YKUpDU{UJf!S+wY#h3_mZ|KEeZb9{=+ z6$D^|fYG2_DE&`uNv(v8PtJI2fKCd7jA;C=s`^qrvB5u_WU8J`is)%D_`1i5!rf)> zexZ2ryB&YHTo{08+gy?q+=KMt+THrWmyIWsBsdXS#!%;^_i>eZ@V@(A zeWK!1$yLkUQ>;kI;w6h$EAecJ3jW9vPu>4qc)7yG?x^z8Q%{vD^;BM;x9}Up@tMXv zf{nHpAFw@AUveArTJjR|Z8NR@=m)KSHS*^VS^k>5g*=Y@--oUHcJiB#SS~Wl+M7s@ zA+LDUy7wYCf6Ve2@(J<|^3$cPd!gCZ-Y4XD$Q{Y42)~y+6p0lOLKR z_S={~v-cRmV z#_B)3*t%~de@ZU&jCFrtiFJ=7Hz1FG&bq(2)VkmIyyaiWpOJ?yv+mQ#^_B}8^U4cW z-$$ND?gK6g_r_(d`vSU`e%bPVx`&pxTyTZ8x8+sK706)~EXR@`Bew&K{qE#pOJdz&q^C!w@Q{@A$N$dTo)|%2UoM)nw;^z<#FWb4=k?(OZzHT-SThb zq2!XQtb6Mk*8Med*_xIclDm*Qk*|3+P^}sLLQ!B-2-Ijchtv` zo!?F0O?G}Kz3>m#p7XotRmslppm!uYzk9xw?EKF8oolRr=XcE`$jza#!3+4kdjZ+` z9quD!=XbZC-(c;%%HO$eLUw*vdluRG9qob}t-kZS+11I;?__r&JHLy)30$;*F%72K z`gD-ohWsaaG`Zj=tG|JKKlwEId2-lvYwtaBX>x6HG&zCXgWMAQNWnHHll(pT9Qg{l z>A4|3NW67V9J>*v8 z)?i6bAM#XkCV3xu3%SU4tACDMg0mb`!*P5zynLVj!q!zVW&XOer6w~!Z- z&yoKm7i(d|FSFCyD@#r!N0Ud8Q^*_1eaN9dS^t^jm&sempOep#hmwo6wBc_imnHvA zjwYA?+1gJbHzxNX4<~1mw~)7xi|(@i&yn9H7i(q1|D0TwJb@fd-a}3y-@Dt|>qGvS zoJk%?-a=kaK1VLF$NDdpZo_}h;V&%53KuozF>*6<8FDvrS@I-uMe+~iYUI7-X!2j= z2IL3!+VDOjzvS?LZG0+|n~L&pCIE|CbfS*`R)+Q4au*Q2hx5rxdYv0saE3m26-8|0rd})3zBaSwfbeq&ys7= z|A*vlaJcL{~)|S@_a<{sc|0KuL{gHy!esgkV@(1K(a#Q;6PhL;{ zo_s&~AbAS8@NL%qeA;`3+>q{{l84c~E4eV;7m&}={WtOry5C#K+8<1Qi(ILmO|P4L z7r8rmJMH1OP&I!KIrho>Xs_7q*8L3mZSvFPMDokDKbRatUPewJpCZ3b|M%Zv?MIO- zlNXYc$UPaqUgYcK@5rmkzmOkecwvRD{jcf%G|v-vlbypg<*yq8?8 zh_!!`T#a0}rS;#IJdW<)k}s1Fliz7!_3ysZ+Ixu{LH>Z8Mjk-_lgTX^9)1T@%dM_4=pbv z|3vq*J-IpgBEuU^E<^w8$!XL-N6sLZxX0SR$@o_y|4#Oj_mlgR zC(!>q@@R(t3%MV;@V(al9LA?Sc}4@9{!hp&sNa=5lRTF^n)>_5yBPkRC9VCFdP4{(ABN@}J~lwEysf)_%u^ zR{wo+F{ZBtIf43v$!+Procsp!|0MZ+^1}~V`&Y;{$j8Zk@@n!x@>Pbnn7p0g9U~th z7kk*+|A_u8k^7N-W_g>`A%-^}>c@rAGP)x#M}IShWrw_0eLC8H#wd>kGzY#o7|cl@|d+hlU$BGjvPk>+X)lv} zH~A1bjC}VK*53Q%aPnj1mgIr7Kc1Z0)yD5f^4H|kU-J;z?_-5#43qMDlkP-9IOHp?d~-EZtX;3(@@?xi-1fQ`TN-ay9b3ZIhtJTX>0#e@|)z#j9)U@O`b?@L*7B2LI18Y*5141m&or?zaIH< zx_2PINcV5ZEgAp4GG?w!fk z=sunN0NuBf&(Qrc`8m2j@}do|CF56vyq@l@$&;M)kbUGI$#v-eGWlNe!!KF;jUD^s z0PTN89zdQz4yXPOa(9Mzh1`MqPnEUy>(TuK@&&r5kcZHH0C@`C7m)}1ZGAaTp4{AW z(U-0L+v#4OoK8+4-y5*{{mHfHzKk4BK0|(z{7^Y-{}pm|@+ahWm*fHgtDi}pOZ|i7&D8%-d29aw-J{4`$*ssM$+O6v>HiS< z5AtKLT6+h{waKj*pLB8tc_Mi%c_%rOT&RMz_fVQm&)ekB>FyyfA!m@oQmy_H@+G?e zM!uWwC111lKc;(S@-Di!B6p(uSn|7!&sOpty8lhyKz`=k_Th_jx{1$m&8(V&j$m7Y~$d8lflV_3llbeulf7{wGOMaQW zjQk<_3vyfXyW~ma=(a{bhsh6&bV~{$J!JMkp^CtNs-96;Zy_-e3D#}_8thg_HVQ?`gxDMiR>kxArB_6a$EhM$n(iX zD_MJ+$x-Be|GmiJlgMYtL&=lK>&RQl*T`RyU#Md3$B>(nE0ZUY-Q>gM zXUUIMwfZ#wmy^@U zUy|pNPm%YNdwgc&bNl<&e-*mFN`9N%n7p6dkGzHcmyxTHPm;eR-~WNN|2erTIh6IK z1^G3)PbQBb|4e?8`nOfL_C6xNOn!>`pONn&_agtr_|GBVM)$qsMs&ZUhP8i)?yr$k z=$=4cO836xuNa?2QDFQmO2Mib;y;-?a9&P8RS&*&*Z|4-xcybWZ74f z^@_*H;bgqtO+RjO1@c$q=g6`zCjP6F=aa+9d&u?4R~_zT?N_L0{l8C6BR3*{Pxg_+ z>NCC_tpC@@FOrkU70DyWm)cwXZRES@ev@2~_TOq??N97z^;?sVkbfYbq`m9pFxq?d zL+d|+`ibP5bRSBtP4|uD0pubLt^cvquSPCM_jcqmjPFA766(vofRslo^5Y*_{+Qg5 zyq`Rne3ABdk++i{_}KbiO>Rh@PaZ=44|xswciK0fSpOTyZ;?w=KZTr0_Zj2>-A|EA zGQCfJYVCEPdmOnF{SP5$kk^vmp#Q(fpVR#X^xKkOjcBhixhCC5lKavBCi3l_ZT$%Q z%=$k=_lo4L;s0^PflSJK`xa&_vIKkc#h*3lk*H$vk#n>?BPJNYzuFZqSWR{wIcEe|*OrDm4kz^P3w~?PCUnQ@j{^NdY z|0r43K_$HA)NfCYC(k2SC7&SIB|jFh{!^Hr(d7S<+mhE)e;Ro=c|Um=`LPSTYmxjq zM*Xtn-gIw7E=e9peuMT_lP}Z%4f021IkzJApLgO%ewOvGFFB6x^T}V)|L^1@bbqdu zwKtROCa)lmB=@5KBjkeQ2hy$o?bNSLzDVv#&ZPbtau>8S`CKKRAiwm5wKs`QBJAh{g*OY*0VJNwg4xE;$$e>m z7I`dr5BUkkNA{_tzMZE3GUSb@NAmfIyp)XJcToMk=Z!kSykk63EkoS@C(+R2{K|W1hOun~`bx&h@tB^k+wuQUPbplBoPkx*HD|s?Gth2Qr%kn8hzD{?U_eg$4vAkQ5 z8PF~c+^8eS~ zcYrr>G=bX2rr7k}F*Otek_(uQI|3KsMhgg8l5JsGGLqaNG}9pgLJJ*(=_R2<2pxWE z=p~pIdNU9@p}*PNJ?SJ}Gx^JV??YcMcb3cV}mJ%a6zWU&FW={lW-bjNp$V z@FMaZ#Z{pEO(F1g0_PHbB_`nS;|YBLfsc~!nFM}D;B^GPMc}Ihu1fg3PsIGH4#4sS z5_kuJ(+GTxz)J~yoWS`6UQOVl-(!Av2>-SO-cR6E0&gJj90H#s@Nohs(foeE{IUof zMBq{cKaIfM$oC}#PA2(1M&RBAeoo+Cguc!s%zprZdl9$`fkzRzHGzL2@MpsB4uRza zt}q$%>qX@0PT)BN9!g*|4@19|1RhM_(**vR@Oww#Q3S3#1@q4!a2En!BycK$rxJKJ zft!%@j}rLj9ytBS1kNRJnIAEKNl*OUpTHdnoI>D=1pbl0^$5J3z|-R~eLjH`2>;In z9!lVbQ*nCj2;7yxX$002IF`V(2wa80y9wNzq<@dVr^xs6(=h)V1a3p%2}Hhc2^>!7 z#}N1x!T*K83km*70>2>eYXUbU_!Xz)^kf7MBJe7LZy@k)@_hk;i;(mW5!ji0e@ftz z1g<&*^FK)Vg%J20fl~=Qnc)9K;HLd>dF&&wKY~L*Rh~UPj=B z1RhJ^!vx+(;CBR`L13>rnEzpto|3?)$oJ_4_8{;c0?Pj~o`^lMGv zL<09Ga0deC5V$LWmlJqcD@?zIz$4mV_zZzjoCf;cA@E*b41XfbB5sKkx z0?#Mk2NU@3VElbHfmP)DZv@VUegpj;5O_PmFFGId|Bk?Q2^>%0Py!zy@+K1aVG!mw zguqvbe6t9=gut5#>{%V-pQFDAVE7(^|NIWaMHXOr{v!6cCV~5>2>IfVIKm29*5y*0=F81VIzTu5_u*PxF;#kH3a^~gz*m& zc<(R_-zIRS1PpsD!s&mbz;H7HBfl2Bm7noxF5m4K;Q`k-*qYG z_nF{(6F8Q@Y65>C_&*W2GJ*Hg_ym4V-~lAPI?FKssRWKBFdBEDUk-tHk@VIQSW4Q< zMFQ_0iSa)Xc*qzG*ISPHFCq0kfWQxP@b^Rl-&J8ahrr>4-y#BUR^#vc37kmk+bsf{ z3BPv)9!J_w`4u?*c4Yh}Auw7eML!vV8qG7^s2!WpjWBhppUPs_x39Jjl-!Bt5 zJ`}?*2wWG&wdhxMC6;Fzf!h(dKB=$$2waSOA4=fyr2Q@;@B;#$C-A=r?6eB=|AWAF z2&{&2I{HNtcs==UCh#HxPbKhmg1?2pC&>33^mhU~t;Xs7M&L#S-b(Dh4}p)z;Pk@@ zEbort6as%s;0XkNMetV>_&k9R5%^v-roTbp4dnZ00+%OnwKZ72wp}s4FM(ra7>*%u z*;owc5O^;6zM8@Nf(LB0G2m#<$3`-VWZ4VGI2kJNT9z{Mruo_}Nx|PdnJx4(^I!i#*@j!3H~c zkR3eS4xVNQ=h?w)?BLCI@J>7UfF1n19emafzGMgAvV$Mm!B6erzwKbBUu^47aXYxQ z9bDHAZfggJ+rer(IL!_oWCu^OgO}LBo9*D8cJROK;Ny1i4LkUa9sJo2F1F5AKjrM; zYIbm4JGg}%+}aNAWCw@Y!O?c`0aqud!|)t|=XZFH!*c?jlkl8^XD>Ya;Mot)QFxBQ za~7Tp@caQ!K0KG;xeU)0c&@^84W2*Yxem__cy7XT3!dBX+=1sVJon(a56=U59>P-q z&m(vq!}A25r|>+3=Q%tt;CTtpD|lYR^9G*3;du+sJ9ys1^8ucZ@O*;j3_R!HISaRtGd%F?{QqNqQN|Q-2yGYYY+boFO~U}G!?YFDmjJoU9?kK+BR)Q{7tW+s>U46Y zIoZj|O@`^y($IxCh&S9-MPCfbk45gHL}%OcBT|Pw^Pt#XYRQA>Z~-O@u1OIHnFtOK zLZ{)wg+g#;a~7L?fG0CSUM-1pA_Zk9XpAalA{2>5*A}f&Y=U(vVK(80%y_Lq2Ny_l zGqT=1&6$=J1zjq}C6N^iU9B0aOx5bLW8nToIJsXejZvfNq0ywktuLGu@c}`BN;s07 z14FPfnMi%(uSqmkR3@B9%`OW{DFg0JLTBHj8;$6jfK8m}aML)tybLZ|QF2vJDqVud zWXj1CDjmLeg0)mot<T8pLkfY4?~<@@Y4*~RL-eUDW)hVwrr(~ROKe1VOuA7dZme8TVwoPUV-bOs z0gcZ9Yv4jTicp$W$*D@}>h?n5;@eq-i!CDE1a}SyGDcYVDn&tD>(x_&sPv7Na0LLm z@l#k@AfT>+6_VF_>!Y9y7&QjU0xpIY#D@E(;j%DOGMhEGuUc_ooc7GpTKNo#4U)L( zA7|F;gtdt8p6AdZv9x@GL2*%5P$mzA%K_QOI7AhI*h6Uq`<_JZm+J!8a69VQVvH04 za4}g1Tuz1lHRyr7P{`rvt|LB*U=7^nZJR)0XmmP@Tof6Lrmb0l(!r76|y6BjEc8ak7~sihJKBohqf4l_W}O18ll zrDYomacX8}7jkdSpg?BAblGXiK)|fiB1uG7d#dY<%}&!mI->F)VL)a=ME%8A+7wQF z3ynDW(dEB<3W-h!8vu2U4+k06kl-ML&cJKNVv-G+q{hpYdX0{kB^eL%;+PF-0gMJ4 zVMv9+NxI|Y!dhDtu|}odq_dc6VY3zsW-v(u<(dYUBg@luaMv+UD<#-)w;j4CSzS1) zcx{3aUx;U)0vXI&`We(3qF@}VgW$eG;KEH%TC*lti)?6N=&ZvM+smj6&k0?;1(&Q9 zCh=GUYoE@el2}6zO@f?lSVR+17dl02RmpOgT(i1bte7wh3(_g|LCH#U4;VHWGG%&d zV#ApSdhU*>kl6v9XH#<}$9{oMB}AW|s)4cx3j!D0X2QS&x!E{VKsFu3iO{5i%ZbZ* zMe|0;;w2&2BAHsHV4n=y$$-&KCcdH}0*KXNZbGb%Ars}ijxf-Nd_c1M$4@^JSj zZ!sFsHJX@4ES{inxDwIVXL8vGP&7_gL|J3RAWB$dd-z0U;<9?8FaqT^qjA&qyp^4S zhQ^~YhIF_dR#*lruIMbH%u&~MI{GRd#HR0_7F4iYV>B7`><&Z+t_~OdygJme;pd%A?^{`0B z7+Dr9T7FD`4YH|Fi4Yngdr$I1WkSg8g)5Co8cYE0Zn&|TuaqO;dgSy}vDmP*f{eHc z-NGEF2gf2@d#p7Wp@$=_fxC}`I>M;id?FDv!Rn0f)s57uQuG2gDJB*Cm16UVh8yUn zYonH$&2#4n?z+Z2IWvO?T!l2Aq_TWhaXTCdI!?_I3aq|FgE5tVS`Y)CZaTX{vcp_W zrbm?^-T>pG7?`K9x@c*gqp2NqLmE9yLdiS9mJT#xS~^05!H-6bj0vYI0E<^;C@a{1 z53?hcCe6&+&X`zgwH2Zkqa0hf6+sb1-Rw;Y1X~;v=t3x!V2w%#3t~cOA$m#}hukv+ zASfJb6bwXkOEJuJ6Sc-v>Ma<4$59mnztDM#)__ff-WC!e_e*mvBr;OLaubckfQx`H zuQtf2T7(-C5*(+vHtbQJ3nLCyL6EM5%N~};Q?kO^EVzD_HL; z7b1eL5jAz@{pBJ_P@R^kqzcU4h%PdFG(H6j})U(rL(w5tTjy$ zXcd#yiO59NfD=C;-JBc)H^#HMm>Ia()FI?p7*nw^k(ZEKx{lK;K_+nRX;q+TH7Xrzo981B(W6L@E+DBn z0sL{iGJ=^EsV1;xDl1dI^r2ldcnT{O@R5qspjQ;CuE?(+smU@&rJIvr{FEeD3Y8cT z;4z)daWR-tuz(h$LG}zrMCnlZ1?mh!H*1usa3?xX1;Ac$nO zeNxFNI53UDFqfm62rfYZ)`(Wfd;n7lO=D6Swcr6~S);NQ0YTvN;#D{>wX{hp;vqxn zxE5f`mNqj2S&C4mu}+R{nMEkGXgkE_c(Ow_sM$gnZIR?klPS|+gvqeMtWklD5|9({ z)afeTjs=ty4YpfQ;kc+p#c@Y%0Vc!YFrmP z%ppT#J(MOi!3MKT+8&F-7y&Md0JRz_jGASU+y)D|&Y|&IxH=R96C0eFFyDg(|Fi^y z(&$)VHn>zb6lgSLLPmMHhGaqKZPs*EW+-us;l)O=c;N;mR4}w611d=aubiU?k10wh zEJ5({1cMBbcqsv9xxu7mZLmysVTM5HIQSVFpg=Q)RK&WB$V{ngOd)a!%^loK{Hg{V zw9>*WJK)4Q`Z$?lA@#>ahx2lYfDS|xY)FT(1k@J+brEC%!6>W#R$r^u5KeGEUXI}8kkw7q1MBz zmw2rvlf(VjB{o*B2%;=2El{mo_N+ zAi-qNLGO?1h9TV?5yQ!oL4;~O%;R{<#5NI8nzJQ=Iuiw>KH~Axn7LY*Efua>qU9pt zvvj}%(SaBZL@P6qaHxujLe~sT*BUh@FnZcVEyUd+;|FW};2ID^j>f71NibKF=@T7& zkyI#`CCml~je%Mm7<1r-vqo}aWYIyK4(P)1o0tU38B2_#ngl{O?PyTp!!8b;qY`Zp zg#$LN=#0=TR_n)#DJTSkE6%6mT+_msxQY0KGnv>VE|a*cIpZs>BzS+2P(^`~fx#C{ zkAwm~EsSkKB>A;xgDRvx8)QNKp*b6^1keexRUF$dEP{&)xOBkgiB+`VQh)G=Sz2&; zA}lR9)~Gh(8)flfS&%JJmCP2#VHTjYE$D(7W1TqRWI01G@n>kUoKcqqLnz>qMoTwC zT$|%mihLFuW{J_oMOf0fg29zGh+^U@Z3wtSDq(3JIspi;LEZ%z|D_saW{u<8Gdmia z$)VW=C2LeEFsgvnA}-IUl_|6+6NLT=BpnPvibn7ezyJ#~W~q2|@_-9%G+PiaBuka7 z)F)9%^JRzHnFM_+5|JOz49fu`utIbYUk+1gi_T%hu7x0EaCOOo1yVrlzZe}7gt;%v zf`hd>jg7BbVo}wV!s9+h2r9ke`zGb9kw3H+UAD1x}SWMQe_yU4p zL=Hc=jT$f}8tj2PFV)f2!a#<=YqO=y`2;|dO51|w!^VCE5oJr}Zame`#9#ji1HqC( z(-BI3sBacOH%~Q*5#6@I;?o%o1Q!l3PYWjd^02jF7)-$#-^y1USKPtz;UY&Yz3)~C z53gsEyuo&6K749Tz7Pb^AE^bc7u9Kj;{cUg_jIVKYzvcCoslqqx|AKnWo&+wGiK2+ zUql}EP}qX5QO6pf@qokE(SgEGO{iF0=1e*iSYt$9cFLtp^>NgzSOaUQ5)UaI^HK<* z?Gh&(qBV&|jR|cRPt(C9luv2V={k*7cnT_n(paefS=yphj>{Hl*P``8q{>2F=*q&$ zqY(0&NWc@dRiCejaAGt%hC~yg5i->j*jF_fwUJK7LctYs#6ly3DnwN|Xo6z0f|@E$ zPZTy)%L^e65k|C<2)=lUh`)@>J<6gXf*PvK0B5&ZW5Q_(SqQpJXllIE{)=cyi2E1O zQcK5rx+WD;=W`}Y;ewhd`l7>MQtcZLWIoL~> zy-~L5%NUU9zp(Q0`UiWIBeRf#HK@y^@K~KeOk6OPqU(?yuAn(a>kpI^TstG|1fg)z zj)4&H967po*|5dr&!ULJh=CZ3&!9D2fw%6``rzM-$Cgh_Av(7hJzNU`siFj8-<@VB z2(%MQmBd=`GKSGFbP>pX4r55xuv{i{6x1(_MMd-rqv1K8=t|8rXM*}=A`4k_7{9|g zM=0;8N=KM@M-^ys2t{IOFa@t}5h`LUC@qaBvUbwws5DS0q$nXoR!o|=*osNx3s+1U z6Zv`_CAJ>vq6kFJSlHnNuA`WA2>v%YP8_LoVz8$do#aN2LSVe*O@N?N#w%E>hEfNQ znP>_)IF3GTOf*doEgY;QoJ_FB2*F_xqXBWvuu>~FKR6&Z+yLGpmT4zLvltOg+)$7y zuk=<_*i4p*_~0>ij4U{2>(I>v(|v=S3xV$CZp$`VRgTxrtSB1?sm zR2*jtRU}&!2UzH$hA|A@UxRXsDlnT{g;_8Z0VS3gY-ppTanL&A=;0Za%1UENhGj@# zO8`UBG*k;&2gA0g^2jim%Ai-Y^37uLVllG{f)q+>k>X;f6w_t|LCBLafF<0LDUBmp zh)$UXXTQNl0B{fSHxWnTSfK`sNkkX{jEFRq0P$EH?qJ89h-!zJcRa*Pu+4H%!6I{w z8#YIaMhHka*4aJ90S0Gcq(_5VUPk$r$xy@(*84qGfd1sX7FV1Fy?-V3XmX~9SWFlmv)tlP*o zUg0E?dpmC1;6t{n3}SSED^jchZI08Zg*FqgBu)kMu~HXSQ4}X85zD3(E&-TiBDF>( zCx;+U_MOpODdBRJD_M>d zTM8Vd1(S#p)!gk%Xff4Xr%iq;0m8l1(Rmc{(Bju8ZC&~%n{(#1-L zoxM~v5iz-i(F&TgLG+#xP98+tFmN%#HYJwr{;a99HZ1XW1oX?wK^`S%S;ZDF0qb@W zR&tpdRVS7Ocvfs?GcHsvyQf2pzO^ceE+Q0(V_WA%v@z)OG};Vsw^FAXnF`s;9H2w zhERkJq0To|Kzxh=g{cw+@+b}CQ;|eyQ3_e8xU^UU{OdRjwW@+p86dI=mfnF5opy>w zPUzG%)-&DhX+unAZ7L*!h*3}noO_-L=f=ing2xZD;ww9xzC8~VhDsf~imZ=do^dab zs7+!IJhm6u7G0p%!14hJZ0Lg_C6!47m_IQ>g=&rDbOrH}QMqQ8gP4RUBJO}176m{K z*xlOfxX7-G5d^CTRlOiE+bl8TEbOVl3VAPRcX4qbtIH%qhj_5~@$3_W!y8OgHKi-2 z$Qq7YuITz@iw*}$NMAu zAy{1?$M_ujFUhR~rRbbGP>RhZ@|jvShi797s!IJM4x;TW<+No8Kn6ztNCcXP2qDM} z83qm{@)<=OqHc_*psC&~6G-q$15}gcL#JlcXlMe?Y&h#xObRqfA%vp$kI^m=!Kpae zamE~6^5z213?Unqae{CXWTq!J4T3Jr7kVxxPl4VJd9~0^cj{mW)*WKj3W#v6wECD#^^tBnG6$baSNWaU7Y=_vx%VFVjV zD4e{KZqyVe6+%+jl<3r9NRYziIi_)#I-uVnI`pvX0$t!>ISmYKC0=)QC~IJbF{wp7 zSYVHWo!3+_3^yueWCG!kf0nM6=0~iA02Z`NOTEUZkRfUg;5cxnIRgzw7K;#N~ zb3mjMXKfV>Y)})mlnWCMqy%1B5iY%~DmIH~{Tn6c=&7fLh|;6=FH)#O4+h6N1J_D{C)5|a%3v{T`r5VUk)d!V*M6=4t}qYEQrV2_gnPaxdb#k!N7 zhl+J(%#IJ+?;R%!tf(yhBUU_W*u#ko8_LmLCg`*%bm|%$x1cp8Yt#^drNOI39NMwa z;lA3Dj>`v$Wu(b*h?PyU0ts49?Y1F;T_I>Fh4+aSnnMqRF$Jz^f$jGY0WFpqrDrQc zVJOjsZ*U0XmnO`(Lr#RuC|_990wN;BkSv(13TXi$LR&~;*`;e+zz7kYONrKOxLP({ z84xb&`$Y(e&;m@^ddJ&{9Fw=;(iTPpf2w#y;PpTeykhah3QRGDIT6g4@ab@36CgC` z4zt3MpzgA86qJlX$?1B>b8Ku&04!*bQUxXjZa8B1m~i8FVc#3=K!7TaNZtq)nPYSm z0ox%Tth~iza{G`R7~9wmuFjpC3T=alhyF}7W%qOg%P83)ff#61&_|jRp{EnF0#LS4 zs-h)8xUl0DomvCCj@YkQbdbl!%oZ-w=nQ5H63&5$D*%VnOna+0!S zzblIsm1+3}S|0Lj+2un}AfPEsK?1sso>40i)t6fOA)DGPkRe7r5RZxCU-6(?m_|e= z5(is=6VPd437SHQP90ThLjiLH&SUy)i=Kq`Fvm(r(L(ugOp*#<(PVxZF{)0~H0d7+ z&U)OSV0L4*<}b-_JY3Ypac(JFk~USDHW?0DhI5%pBsERpk89Ef*O ztK*}sFT@d{EA6Z_j^>r|4ZJ1{2obW3pBNZaO{;Tm9YEWtfVj`L@NB#Z&)4d~L06ve65vJqXZM&UzU*$boyMVr+p{5{Uz5pN77T)* zWUUUO86Br>OhF1KLj+v7u4Z$k+p$E>Kznid6#@)^h@lQabfN{eQ$p>e0S_SC9RYS& zs3L$5g5~;Rf+(_lQG-GC6ivNYiwM@}G)j|(E?2&Gh&u#wfQz7V2)1Cyfbj(S28@I< zg4RR$?XVSBq>P9yUo9p&0mXAo9Z{Jem#r8r!gvbqE1eOKjzLcF#W1W}TZs6A))2%nQZ<%iF@fSk9a=-=hBIMQtal?}OhX!-!9pV<{}0z$ zSrl{Xj2qOt_++J>1_+N`Z|Np9Zl}jF>}A04K5#O^QZQOO>vSJU?tsZL^oFjBsuqd> zvl(2nun!nccc%_w2QH48v7l4e>+s!^gQ@}MOlO7lIBhxB%~{Dh16EMW;qK>p_`< zGYbyF6-|it?TJQaUBa{$At-<@df0kxtyrk=S?r6Y^neb>lcDN^PkZMV$%;)*G5I1Q z_A|F^vSUq11TrJXq&QeZb8He|&`?@pU%=oZ5iSn~{}{M@0{WO_gX7Yg4gUxT7N#pN zqvEqqvY2IvHr#zVF^WU5v@*W74z&fQ5pex-R3c*^EWdpK8dSH~!$56vkI>koAZ^eP zz>t@}y4oSbs*~6jPNykCa2i!ec$psGRKt*-a~+1}X@U)n1L3AAdgC!==6Ui=uyN~Q zCWLJvj3oIA*oInUQWi;hr7}^o1R<6%=wZmr!w`**MARyRCR8wJ<%w&=W@%=)mk;2% zAOhd&G5-9}*v z-r7cC!F`l)?w%0>h^Tu%*;N<9e+L@H2#lEUon{tHI#7!zQD7LHZlq$=(99Limu43Z z@Iv$|V|E%uZ-5t%<2E%qW#p_6RH~CS?AJrnXkDm~Bm{9LO1bRK_}EbP3+rfRXg+{O zpAam;pDI@z6wL_zi&h%oie0u#k7zW0rBYv`;La|%`#1*9W`%$jwy<7`8FDnEOAPTA zMLL+8T_waYiY*I*PT3Vyaai4GsPTglGs13nIC&Q~wo$tz9aljpVS!be7`#}G5w5Th znJa1z2h}Rh1pPt?I!u{8_ecW4tFG|QmcS^mW>6&}H5#=EoKPlKre>`U!b;JZ(P&ad z?QuupvW}@!h}iqXJ`bi4Q1g__f|PLGH5}Lpj9>v8E}!RMXT|1nU0Rr?rLhF=S$0$f z#e_u5Q<*uwxRo?{Eu6hE#z{Gx)l)<%UeQ_DPEdLsmEqhq^}>&ZeMzUrf*_;lTT*SB5KL=TkCgrGxcw z>ds4jpf*Vkx5Ps;W4)&etA}92Y5sZ`WXSaK(Hy#$nc}0-tt&7<#Nl&*&Mz|z>S!Zl zAeW#(d@w9Xu>?b6Vc01+?~1b!eSu^>ltz?cvfCAf4RGW;2CgYUJ&%aii#v(vV5KRU z{n|dt--K51qU52FS2GCUILfy{qISk2j~q)RlLe9bgRSer7~G5ZP?MRt2GDXRDe6k> zzQrgLZL9&VXQbvZa3hrZbfpf>@0Dg%vLY11N5UaO0of>YR1Y^MkHNR&37BXqEzA-% zR_+8@?y#Ud>=D$p^RyEi9igExL*%#KWWU9dKKu#^;?PMkKiXxSEPj!mBsLjBSYhXS zDqQ%$5EdAsQ3n*gEHM&x^1~*6+wbXmS^>;j1Qja~_@zLFMWn$Dj_@f$tJemnr>22j zvh&r!$5>-_5GpfPwvKnRQFaE>K)Y=e84`(hdxvSz(p?=5>c6mnvu``C8!v#1b8T$l6@d3ghr_@1CWEZgW zpEdjVGwORd6=6j|w{9UWKK24wQLIP2^W#}}q2XO^>}6o(YsE(bwBi;3P30+O4kc}6 zh8*pnD_?Fd*la^eZMbm|3kOXwZvg~yKYMac6)XJD5AHcOenIb?T~1UoKN0a4;& zJckzHASb~SFD%PGXnRTNEz?3JO5;J9QrF>VRe{-XTDpB5WXSADA*eP639e}3*i(73 z?SkyenZcMK+Z>O`r4-04y1WW?&6(hm6P1LR%1nzRGeN_LgX);Wb}m_DCNbqsa~4-k zX>6!ig)CGnh3G=1YA@>kL=-CE7ACiVF1`*}kww)3KreJ15N>y5MgTnp8KiM5g|BzD zHaAOm=g^;7agc3yurg3nz$wGn5zW>$&|5^|n?tvQ0+R4XPS(Byf2MbHu=Uy2P-UuL zbI1Th2D8b;H3UF&3VH}3b?9j+_gRL-TVk(pivk_`O+3G`%X*yNNRFgs9hKXDwi}m& z4D{GJ$OzLoys?VeE~v0PDYP{ z5YfCO0JhyGF@0!u-`VCZb|@eQ?e{UDh#)lFcCa3NGkm1%;F`qMXL#Mes)qQB4eN^ z0*pzpqt^0w2K*hT&xD0P=rquki2(PGF}HziU?)F9=p7p$DU~P`s;n$!f;K}cZ4H0x zlrXV1!%^}IEd+9hu%LMZiAgI8Q>sv8q_U%`b#h7ueI`U>DHI8VVEB#xP-Q6WvJ`OjA*=(yc*~p(JqmgYt`Aj;LY17NNQ9*V+c#8N_!X=`8Pl8~3kI!|BEbN+ zx|og1G=&cQd5U!SV11QF39Htr@UCCDOW0^cI}6l0IBy=zfLdqD2Hhl*?}xEKNIh222_n6)C>Qnp%G&W9Fy;)&{1w$1qu{bfP2UQ6^8i= zLJZ7M=uj;|hs+O{H}j2NlL`936C3hA6n-VW7^|sevdxL6Lyc zfYMU*j+A;+%N?lajz}_kd(&<9ar%W8VL#mO6LNW01rq zgD&Vq0->DPc>2?+BSd8FbY=%SC{zbwynWz|Aj&Cdw=fbS_x54^pg_jOp}ZOL3P~XK z4x@7eHHET&-u{d?V65@o1@t@n{8LKxkA`Nt_!IiU(rEbaW z+Avr7hbKrvLM#<7L>?niM8Ff8A&-$NsLnF-OPET9nA(aLEDfRRj-4$D@}^(=Su%!~ zDGHT6!X0(+a4NnEYJkErY@q7@m*f!C2IBp@^ab2}pk7%_s?|uyAaX+%u9dd3U?h;X z&x+$Mfw2=^VbHQL<*EAjs8K?*VNE%hF03!qrlk&iDZPMrmMX?BJx%>3sY7l5H`WE< zW>XiwyzT=hi&mxojLkn(k420xRBteyevH{ip^&yxDBw&QjVv-oA@znKxl)&|p~mFBHCT(vnH04G9TP0EHp+Hq?L(9nvlX6nNKK z4GV>Et9pnWF@s3}N?oBhz{SFVAkPTFmeS6s(G5aK=IlbqoXwOu``;k5kZ}JQh59DK zXcOri>C=zs6Wlrw2Z)uOc|1geu@aMk#p1T$^eY2}Zi#n+g!{|k-jKa?v_ z<3DLk|Ls!1<`}w@|EM|s&*=K!(src2G1(?XIP6q_Kt8y9UfxZWK+WZ0>PH>m4jwgz zyHc(+C(BjToGV=F8?P{7cZ|XW@msR+RH?+pk`BW8MG&=C|DXhL!Iq=4ey}fPmX~!{{Vzy!biWXti z5XoejmQs_naA|7_9l|fEbdlee(iNNZzLGXDbFou}kG(WFXbL5T1qsHpbw69NsN!K2 z$v%59l?sL6CJmmRd)q0?aRSdKxzx`=ZdNpJQD`g`B2wxf1WN|727@jF+>F=}?u$y2 zo@cRq2Zsw;??rVmM6ODeW`F1d&fRpI}2bbbl$qBy}%R>XO| zq+IwU(hW#_FbR1<*$2Bu$VdgnCj*U?B!fUI9Zm|X37}SX*=X(BmNhj-Bo)xL#%VHM zqLh8@8Zaw$3=Y?l%e{Ran2>K&E_}JPg>#`as**vFR+xwN9YxEjX|iY!ghbQi1x1DJ zm#y)ngs@q!2ii6`QJdEk2kcH`;ecucwEz#)ewfiM*n~K!~I?^5}kkS=r zdW`5lYkG{ARC=76MnR82MoI=jRBD{~RvocZdUNKC+PGrP8QI4I7KO7s&`K|CnS-S^ zAR}dIeMjYsv$jJJiCj*qICbm-B*ckq%@i9MXFG(tiDH*QIMNQu3;yzAgrNc@sUM7& z6g6iXv;>p_Eop<6SBTP=lhT?=3jqfY?lPnzW`K&+P70*T!EO{{wVAQ;a=AaY6|wP@ zfD$f~2v*D{UR}zLaB(e+F0Y3KvGg|tnVlpIE&QU@JVzQ2tf+h;S=KI$H~d6 zZU;gEo<5vS$jXUyecq%X>^|`8$10gsKh}2?EvE!lJkk-+64-rkf>Wp!nky#UQG^I*b?Vcrs{cr0a0Gw3#1wIgBF!|2YQ}hAj|b;C&I~4c-ffZ;3DiyyI2aRZ z01U7p9+qCXp}PLq_*~Z_Jq2h)NHS z92it&A|r-aiif9ISTw5B@VL}w&LWBq4;C!56)`lpKA`!B7;GsG9Y)NtX*Nd;j{O2i~8#TAm8T8P3Y)iB+pRpAm7LxuH-CKqC`9n>zG zkRFCYM4qUGu*R*^4>3fzw!)l346+nsTWgS5I^2rL+(T4KxL_D_4pCI}fEyhk0b5PQ z(0F4SYo07q4>8oF!cnsfG5C0X7aK1I*BcMq?PnHZNQtepPAS9?5ihQ_os#*57+T^| zwrcTxCr-DyVm={6r6cbGG%z*|a=V!$zUh*C=pC*U=X*P@xiC|E-=W{iv> z92xEVMFHX2COk~0zA_dv>`AP}tO$p-VRWH!lEP-VjlAO!_iD)+y`Ll$#mHLT+H5tU zb|a82+g22KOG$(FzuCM6V}jnnIL9t(%^@Z`PfBIhDd#^Ln?@b zM9jnDVHjl-W66dE(+LxrPLS9}r5=tAfmnQWsE-a}z#w264f4E`6vk zu)L*oWIk2*>6H}xtrh*vYU6DYEX0)S%I2H2wL=M0OnrReJ*j|XKN#MvPi=yux9p=S z6O2jIaYeXr2qo{8vrk7A8h#30XfIhdf}(V=zF7Z@QVB0CJl^rQ4R@x4(6KxY93Bte*` zED?5ND0OfP8WpB1vH+z%Neb99k8A^OZK#}K;Gafi8moX>L2$DDy}MR&kCRi&%kb8f zeDQ*RT>#weh7bMWf%i_{PDN_&sp#xf68@%t zS@a*g|GK+VpCLl41q5|vz>SKD=1lN;wJ@RCiBm++u&!O(`1fqx%E|XbwQIE|Es89% z`rZ0F%|rXV%RkkNjrH@u9-%+p)mI<&)32%@Betd1EWeM*l$IoYaw2}zfF&cfW1qZOv%KoltMQ*6%w5#| zjDL}}&&w@4+U4MrCTm~JPjCK2wejGI!4>@7)?c}k=2r8tdUCIM-|ZUHzVo@E5fd+N zJ>h>Ls>-~2f17FSDNY~oX5gZbf;>=7krzAQGr=Hu+)#hc$PQRKf+2_Y$Pmk{FqCYfu(WxHqE|!2z_2mQ5g?%lB~%I;rQ=i#j#7s`fpzwvTm zrB^Rv(xbQK4)j~UWRUrs_FcoAr6b!0Y<^YgM$k{~s}4K7JZxLm4ehNxO=d2t|FYnG zo;hGbepA(q_A#kX6^$BRZ@%bqh03W{`rc^}n7Ph<^p(_w_vNczJbO8zeeqqBU(GJI z!uR`2-F0&hudZ?{amv}uZ<4nuUsb3*Z2F^R+OQr|OD&4O7V=%oa`~gmo_`V&p##p_+YB3Hbk1qHYjDGHZu!PdBPQ%Eko0zV*t5)MMIZA51AN%Wg~ny>*K1{?(VY?!Ek$n<+*5w(bu*)+sjq(4%DMx)O<@?7|@hllpsaZSB_NU#EL0 zJvy{(_jJCG$J6HZn@(;Uc2DZ2ow99YR>;Jq)6TXT`|S6T&c3^=H}vYT;IqEos*y>H z0!GH(Quwq@yWDy8((v>de=PfS{mPjkuALK_lU*!MS8dDvW$K^5PaksTXy4JYN#U)Wb?1jo>i(ktwNjJLRy^|Q z-praciU9=xw7)?X75_@2R@H3y|Gh-%aS3%C%0FW9#z*&?f1s?VtcU{ z*O%8{)vRfy{NtGkqtAXAYViJPe2oc{T>3q%G$ zR7yBhtV*eh{^j!`vd`c3m^QP)v#wr89{hUX!Y^a|E7iJsX!p|*Q${G`-R8Me3TU

Aco0+KiraX-kLxX?_VeZ&gb;{IBKtDfv%krN+AV>)lrMOOQuD}$P`nr>%n6&(VnrBc zbU$GBhsR<@Jzf5zW^~`fHO0@?Nlrd@=#Q*gJ9F>!U)cW7PlJEDymW8b?A3?HWl)0i z{Lkyn(v%06I=(~Q`r72& z$xh|<*Giv{&(8Yw*LADTF1)^%)iGOt~!dOrCF zCi%A=vml^v&D^TF?t3C;F23pUqQTvr$VxzhdqtQ;Sa__%9wUc+|3&R+Xl(U-=);fM29mh0U6NYE={oUKOhTi>0y)F4{$B)IU`SlEH zP}|*SqCEC|i9dercjZOjiMgk?*7CS8-ECUjlcEJ<^7bANn%6XH%CSCwuWVd)<4yM) z)s(eWt52M8bq-tiqI}%q#@ok5FKe=4WLVpDQf?RGB-*-y8Kja%aC0 zYE%2(*Lsv5=XTsPE;-TD_w(&iv+kWfyQt5`fo7ir30nsQE}b;9#w?!+UbCyWT>59h z($RGTwfC>2pK0_%P``!=6GymIoz>xl`*qa&_A75j{IaE_CkyUGHXFpPEA4jB%B2G=DI;M#V#|$L?%ar~9+6 z7w^mto!($`l+W@t8`^d{shAgf@8#PDzLl3H_F6Qh_3>`o7LV*K>s#*d(x#Wz)QYY@ zZR~^HotB^WnAv4{2hWhD!9PsWX8NVCE&qG38b0L%H9uvSADVNc?A?>DHQY{bKO6I5 z;>{`RBagjIzpM<*avxl9*4)SYe#GpGF}V%h4jfOc>v6I2r~|*=4s4R={7dfzYV+S!VqUR~{cWB!N*#kTL;wf3Fo^mD(3 z#$Fy|?sVc`7i|M#-Xvn(hPmf*LHgngDR@0X4xm%@Ar$6p3 zeRDJ3Ie+KVosX^@dpG~+?)>K3fj|6lR(kzSZilsl%O7?Q_go&^M*n_{=kcTi2P;JM zc)7^^-P?8(3ra0`ymjotx4&Ok_FDDa!#hUuyT9R^asBU|?wIu3yR$(DT6TOpXin7z zH#X;;l&&3K@MrAByj$O9%;<8i)~z=iZug1}-q(KPjg=?vE&b?nf0sV0^qmhmU90^x zef+H&^{b3Ox9I)drK>g%U$EQlq2KVIdmaf{UGKYFH@?|3^u>{NulqFJ^x*fz6M5~r z_Mb7ML;s_?rpp>~i!*lTXj`7w=h~QKR*@kv$83oW8wH^B=BkJG5Un{bj@9 z+nY}uxa7msIp1}8koY<1!QnM^>x^61dUdg;5p!bShHc(gLtD zU39{Yr4RPi{9NXg^MmL4;oo=_6luFVaz@YNJ@s#OREu@Vk4bV%E z3bpXdAaoXWq86VA?TOB=HNa-%Y-wdlMTA$RLMe28bC*aA_`psPO6n~gH?eSYTag=+fq78;EpV6rxs`Rv9 zHkCegxZ|DFsvGOvA0M{+j}paie~zEkq|&|V^MCcrb3eXm&yAb?UmktbBr8O#?VqBU zF!jTN`ghyztG7`aRdf6EExx@1pR8NyQl#pSu3I){weYDkJfu{J;kVMg`wVM)?_%*k zJW6|~z8}--wr=%-aud=NU%6o(Uh@>xfGcXD8>p5z8ba@+7sOh-#H~ zt?XK{N0rBmPhR=yL^U=g+Qgk|OYrqz-yf-?gfw^6-ky zPHDrYXO#H#GUbOBotM1{ix^hCpQHsssN?#Lq@kpKZr$8EV`?Rv&1r30wp1B)EtIJK zwSf6M1MPc$m)rvE z?(4W^g$7G-T-QpI&`acB^MS*#3X% zpgjFw9U4yVo9FakVXN=Ew3)oI-NPxvTg;f#|7F&Rm!;ifIxc<}b+Pu(qh|czy(2zi z{DZ8j*VoJm+&(C@#;C>PPgJ}=?sU(IuYZm|;N{jdQ1I{i?9l8DY41lDS#Mj%;Tk0$EX$1zr#}5oZ8v_1UjF3=E zzy0IN5_NvOTsur&;P$v*_Ne9strqtPk9#z++W2d4yXjsPTU}4$a=&WFzvU&)4hvW@ z>+R{IiSrtylslF8+wB@n_t%-RU{J{=VH5kDtK8b>*nu(;Cn{=BAE{=1)wlHdVT!rW z8y49fH#wqjsb^}x`jxs>>Q!sw^f#~PEjTIPzPG15X>##Ro^Ot=@$xjJZRzAJ`F?BA z$ujdhyl5tCT>bE;6Ptf6o!_tKg<~Byw_W?POlha79_#B?Zec#(zSQM}%B%ARP7RBR z+xtt)fQ^5@zm<0UZeDoE&^qBM@|gkS5111?%8m>e>oZ}*dP&bmyg?uL%*DBKZDbitU>Y(rGa06VG7>6fOUa1+7GxL6 zh%MDeYy^*~7HzlkH0V9e$y$@A%F=6TJQK1#mHKSYG&tfE{xYFsQ%s&p6^b{(G>Jf? zL66=Or2%~%gdust!614yxGFu-E@>dpt36c)xUho?Qt>n-dZwE+UN#A(8Gsfg2AG!g zA*97K4ero_n+_?SZ~_H*LQO5~nkHqwzHK!-ym{8v$KTJr@*;EY^Ncf@lBm6PMvoj_ zV|ItI*3VbY?L5ir-1;fyFYbD2n)~F8y3DccDBp{VJHE~OoN~C>qakfZPK@z9>wjSV z?m1EJDW98t=(XIf#CKIX$Cs;ZKD%{d{dQq_=M4>R7M$Li_Gg)p8{Zs#mYVhW-sUP* zR$Lic&+ll%^Z8{iq?}vapnkbGi`Gmwjp%={MxPQbr*8N=xXaW2{tGI->d+|3bAr!+ zH>Gk;UMt9J`8H-iAN3xUPv)Dp#k}GRE|sZU`qFk&wW!Vct%mI#8<)R)_OIT7?=PMo zxM1j_=&dz&P77>MzUiSdwYEge>!&*0ZFm*=stM;W-TrW@M6uTE($nioa@-by2^&v_ zF>!g{KaNmVJXNEO-AhbuG=AE1 zFSWCCNq4KSLmD9I1P#HpNJ)R2p${5Ckr7r~Pq_FuL*p4_Fv5{4)O`@1jjbG-(1SJ) z8@?z_G?FV>&Vkxy$OjFNe+aa?{OiAiFN`TzWH|D9tftKUb9dJCJ2HO4<(KC-eQcqq zKcvjPU-o)*Su?Y7Xw}PyU8fd(@}b>AeNj!BsxypDoBH<{TBX#=wWg$+&k91ro)>F7 zuUvj!+1sCPcdGny>EP>;zj_qyb#dd1)in}Bk6g{zk@C=a!P!OKcDTNNT_sjtEI-k| z_mwBLO(%}tYwPv4Rrw1|r`DNtzI5e2mxo?hyYAhe>4W-|I<;_jpuuI9G_=={>0>KT z{L4A3LzAq^abpcHPSxzWuxRcpP3@7_S}AJ%rvJA6K96VV?R&j!IP21~mBE9{C!XvU zH@DlkkJWQ~RmwQo{`1g5!G1?d$eynopS|Ef#DwoXAFi%lXEia5E=6H9?IP(++ZktP zS6}GEBs45>9@BsfUkoZ!8pB8-P~zKM4OhmhB~r#P>Iat&hAb&@rxjDAh@^sbEa_YdeMZAc=F9T*OWO$)Wu$v( zQX)5~?lyBf-L_Qn{OWJ39(!A}%R7%+JBIK4=J$!QNS# z;~WlSs4ne0Wu@voVQCZXNb68f+Coy#Q=?ZI)UdYRph6OJ7`vZwsXr31b@e|Z1K zu&K-HZ#Yyff7*~~1vfAEUHkKdXP*M+C{OLFF|=Ain}!~d&Ax5XJYaX(o*y%ee{WyD zF@4$h(#Ku)T;BR_b=hKGJIkkx9pAsP{B-`@lsek_SJ#9eUVJRS|~TkG!1Z0<&<3sua>$vL+b*k60XHy zMh7z}on3Qvt^*{=U@D5bH7(-mRusBKTW6N5XUD3VG3%SG_Y4_5GR3`j*6^)8S`Oax zcgB$!uFrm-KBvntm*N$7^n7`8`I@R#%OJ-R-B`W-dP~4F;OtVuDRURyyD){9-}fFPM+b{s^Y4Oxhck`N=a@>MOOPw<1Wn3?0sL`Wx>_A zo`-(>Q}g!7Eib>bJD)VTUH@pn)_GgKPEU+0Y6|aEHKeCg!iBua51Ji*eY1O!^|O{G z|NQCEj+>{iweLS|dVaos)ct9zsy?`A8o#N=_>-P>hHe}6<8MP#I{#Gk@QhRLdlOb( zy-~;yH`mg0;JsjEck+zHe2+JbJbtb9X#Yh$8~y$EW|M2rYfb|aYOH}~v~B=E zPx#5w4WJ3_Q%Pfbya5BTnyfYz%{4VD<^LyRFZQukafuHagVk~Mk~Ebx&TW+2U<}>C z*!un8f;QaxDCeKm;qZP^>vezKyX#(DUo7CHVtM10G4IaVIxJwE$1+M=Tz&AlrK|L@ zgZ;EGyat_{@_WCeAr(E}Txq$i&AZ5z&OPEvge_WntKN^fyGKUvZ(dX_sr_+i`9%l3 zV}8%_D(|GN(fv%f$7@<7?w#CcOW?BgO}`@T8V_`~51lO|00_Cq}%U(@Srb;=Cf*6-~q zjofX)0O{Zg=Obp0{1`r`S=NMlcV}PiS*e+!T*TD18<%@U)he;SZ=sreesHb75pEY zVxbxF+03eo-pnqW^zE4z3${(JpVaX~;LQ?4+$U_S`oYw><<0{a<^THT@TYCQX}P5; zwBK1ga?UhEo7R^WXkKkny_|AbmfF|zY@3fIw!duq;zU=i+HH2|^-F#SG*xRKtejU~ zxv9mgr{ha~f2G6gq9+Yw8itqHSL1N$tZ_f=t+#D^-3oJ$KAEuK>fk<~^B$be8!`0L z=eys_ZntxPTXvbz=QpndU3(kd+sMsV8|$+bLzl0pcrt!tyl&}(rw4w1`_OYz)kBTV zH;YG%Zxoo_?{)uMAw3tp?=I^%wQIz$f1NXJzT`UiR`kMSdDZ%^NtvZvaPyJ5d2q+; z%8tcChWe&An$>x3`hvrf$K7^kPG(;eU^R;W+eLvAT#Ev&&b+!dFs9g%`WLh2YIeLh z-{--$VKwR>T6}WT%Ek}=zM3|!ZkrdEedg^q23_b7*!`#NYimuKdM171{=V~G9sB;u zfB|i<7Q_yDTruUvg^tq>rw)o$JP(>w_Eg5VbDc`oiagr>_`ADr_H{IF-FEH1@2X0< zKYyFFqU4&nwKW@Wm5~JcKKS{p^V#KB15N4Qbq^Xnr+?WiKYw#3CFzJyh~dbr+PQOv zMUw>KVZ*X_^izn9+d z%dFEQzb*>kl}VwN{&i6RE&qL86!^L*@PE~!fHcRY6O3fqQU0cJ|J%`mOL5D5(mLhM z9r3-Sx@Bq%-lkGEQ(zu{6Pm)-w^F)FVYz*Mokz=_jBfU?Ix8Q1sMKs+{-?Zxnh-F- zn2wi})|7z5UjoRTc3)Bd61%cXdFS*sO7)BpIe-4zi8;h0PL29kWhs^T|2 zH_kKp#>mchdM>|fD4*E0%o*R=bG9VPc6aW1u-Zb;b{}$n&ASn`Ak+2i&xevWTzkLM zE3&e2)5FPg`s$lC|7#dUi;Zv1dywQ`HM1V#0{e`a{`ZyyH4eQGhiT=66Rms)SD zy>0sY(G}-@%KJ2>&GgcTpDDMgDkZ$hetIXr^!8EH%M8ohl@~myXW5eN%C8%~roYRi zW{0|k9hvMke)MDGhhJt6eH%Yus`R&p?Y?pAxAjxwrLyX~tGAl7W>J}wIn@tWX`1F* zvbxW?J#WigJUG3Y^N#ru^D-L_NeU>ktp9trciEFnJ&ND*YWnExl+6)d^_s1o_HD~` zMeYvozKbVv^XsRt8u+S#uNwHOfv+0)s)4T>_^N@g8u+S#uNwHOfv+0)s)4T>_^N@g y8u+S#uNwHOfv+0)s)4T>_^N@g8u+S#uNwHOfv+0)s)4T>_^N@g8u-6i1OEq^hb#;L diff --git a/Barotrauma/BarotraumaShared/libsteam_api64.dylib b/Barotrauma/BarotraumaShared/libsteam_api64.dylib deleted file mode 100644 index 97b6446eef52ff3760bebf2b856e446cbf61616d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446352 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 z 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Ð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 diff --git a/Barotrauma/BarotraumaShared/libsteam_api64.so b/Barotrauma/BarotraumaShared/libsteam_api64.so deleted file mode 100644 index 33762a726e93c776de33f892ffa85e00a9fc7289..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398662 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⊥qb(yM!= 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

mYym}_Kx4_#}r3itdPFK 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 iR_V;bR6#bf+M>PGFr#^~nND4f)36@Tk public void OnCompleted( Action continuation ) { - Dispatch.OnCallComplete( call, continuation, server ); + if (IsCompleted) + continuation(); + else + Dispatch.OnCallComplete(call, continuation, server); } /// diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs new file mode 100644 index 000000000..d1a363494 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs @@ -0,0 +1,38 @@ +using System; + +namespace Steamworks; + +public class AuthTicketForWebApi : IDisposable +{ + public byte[]? Data { get; private set; } + public uint Handle { get; private set; } + + public bool Canceled { get; private set; } + + public AuthTicketForWebApi( byte[] data, uint handle ) + { + Data = data; + Handle = handle; + } + + /// + /// Cancels a ticket. + /// You should cancel your ticket when you close the game or leave a server. + /// + public void Cancel() + { + if (Handle != 0) + { + SteamUser.Internal?.CancelAuthTicket(Handle); + } + + Handle = 0; + Data = null; + Canceled = true; + } + + public void Dispose() + { + Cancel(); + } +} diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj index 26bfe8f64..0601c1f2d 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj @@ -5,7 +5,7 @@ $(DefineConstants);PLATFORM_POSIX64;PLATFORM_POSIX;PLATFORM_64 netstandard2.1 true - latest + latest true false Steamworks @@ -16,6 +16,15 @@ ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + PreserveNewest + + + PreserveNewest + + + diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 29d8dfcd1..b51a96383 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -5,7 +5,7 @@ $(DefineConstants);PLATFORM_WIN64;PLATFORM_WIN;PLATFORM_64 netstandard2.1 true - 8.0 + 8.0 true true Steamworks @@ -20,17 +20,23 @@ https://github.com/Facepunch/Facepunch.Steamworks Facepunch.Steamworks.jpg facepunch;steam;unity;steamworks;valve - latest + 10 MIT https://github.com/Facepunch/Facepunch.Steamworks.git git + true - + + true + / + + Always - - + true + content + diff --git a/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs b/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs index 6fc870701..c7b55f544 100644 --- a/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs +++ b/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs @@ -23,6 +23,7 @@ namespace Steamworks StoreAuthURLResponse = 165, MarketEligibilityResponse = 166, DurationControl = 167, + GetTicketForWebApiResponse = 168, GSClientApprove = 201, GSClientDeny = 202, GSClientKick = 203, @@ -51,6 +52,9 @@ namespace Steamworks FriendsEnumerateFollowingList = 346, SetPersonaNameResponse = 347, UnreadChatMessagesChanged = 348, + OverlayBrowserProtocolNavigation = 349, + EquippedProfileItemsChanged = 350, + EquippedProfileItems = 351, FavoritesListChanged = 502, LobbyInvite = 503, LobbyEnter = 504, @@ -69,11 +73,14 @@ namespace Steamworks SteamShutdown = 704, CheckFileSignature = 705, GamepadTextInputDismissed = 714, + AppResumingFromSuspend = 736, + FloatingGamepadTextInputDismissed = 738, + FilterTextDictionaryChanged = 739, DlcInstalled = 1005, - RegisterActivationCodeResponse = 1008, NewUrlLaunchParameters = 1014, AppProofOfPurchaseKeyResponse = 1021, FileDetailsResult = 1023, + TimedTrialStatus = 1030, UserStatsReceived = 1101, UserStatsStored = 1102, UserAchievementStored = 1103, @@ -93,11 +100,10 @@ namespace Steamworks P2PSessionConnectFail = 1203, SteamNetConnectionStatusChangedCallback = 1221, SteamNetAuthenticationStatus = 1222, + SteamNetworkingFakeIPResult = 1223, + SteamNetworkingMessagesSessionRequest = 1251, + SteamNetworkingMessagesSessionFailed = 1252, SteamRelayNetworkStatus = 1281, - RemoteStorageAppSyncedClient = 1301, - RemoteStorageAppSyncedServer = 1302, - RemoteStorageAppSyncProgress = 1303, - RemoteStorageAppSyncStatusCheck = 1305, RemoteStorageFileShareResult = 1307, RemoteStoragePublishFileResult = 1309, RemoteStorageDeletePublishedFileResult = 1311, @@ -122,6 +128,7 @@ namespace Steamworks RemoteStoragePublishedFileUpdated = 1330, RemoteStorageFileWriteAsyncComplete = 1331, RemoteStorageFileReadAsyncComplete = 1332, + RemoteStorageLocalFileChange = 1333, GSStatsReceived = 1800, GSStatsStored = 1801, HTTPRequestCompleted = 2101, @@ -129,6 +136,10 @@ namespace Steamworks HTTPRequestDataReceived = 2103, ScreenshotReady = 2301, ScreenshotRequested = 2302, + SteamInputDeviceConnected = 2801, + SteamInputDeviceDisconnected = 2802, + SteamInputConfigurationLoaded = 2803, + SteamInputGamepadSlotChange = 2804, SteamUGCQueryCompleted = 3401, SteamUGCRequestUGCDetailsResult = 3402, CreateItemResult = 3403, @@ -146,6 +157,8 @@ namespace Steamworks RemoveAppDependencyResult = 3415, GetAppDependenciesResult = 3416, DeleteItemResult = 3417, + UserSubscribedItemsListChanged = 3418, + WorkshopEULAStatus = 3420, SteamAppInstalled = 3901, SteamAppUninstalled = 3902, PlaybackStatusHasChanged = 4001, @@ -187,8 +200,6 @@ namespace Steamworks HTML_UpdateToolTip = 4525, HTML_HideToolTip = 4526, HTML_BrowserRestarted = 4527, - BroadcastUploadStart = 4604, - BroadcastUploadStop = 4605, GetVideoURLResult = 4611, GetOPFSettingsResult = 4624, SteamInventoryResultReady = 4700, @@ -213,6 +224,7 @@ namespace Steamworks ActiveBeaconsUpdated = 5306, SteamRemotePlaySessionConnected = 5701, SteamRemotePlaySessionDisconnected = 5702, + SteamRemotePlayTogetherGuestInvite = 5703, } internal static partial class CallbackTypeFactory { @@ -233,6 +245,7 @@ namespace Steamworks { CallbackType.StoreAuthURLResponse, typeof( StoreAuthURLResponse_t )}, { CallbackType.MarketEligibilityResponse, typeof( MarketEligibilityResponse_t )}, { CallbackType.DurationControl, typeof( DurationControl_t )}, + { CallbackType.GetTicketForWebApiResponse, typeof( GetTicketForWebApiResponse_t )}, { CallbackType.GSClientApprove, typeof( GSClientApprove_t )}, { CallbackType.GSClientDeny, typeof( GSClientDeny_t )}, { CallbackType.GSClientKick, typeof( GSClientKick_t )}, @@ -261,6 +274,9 @@ namespace Steamworks { CallbackType.FriendsEnumerateFollowingList, typeof( FriendsEnumerateFollowingList_t )}, { CallbackType.SetPersonaNameResponse, typeof( SetPersonaNameResponse_t )}, { CallbackType.UnreadChatMessagesChanged, typeof( UnreadChatMessagesChanged_t )}, + { CallbackType.OverlayBrowserProtocolNavigation, typeof( OverlayBrowserProtocolNavigation_t )}, + { CallbackType.EquippedProfileItemsChanged, typeof( EquippedProfileItemsChanged_t )}, + { CallbackType.EquippedProfileItems, typeof( EquippedProfileItems_t )}, { CallbackType.FavoritesListChanged, typeof( FavoritesListChanged_t )}, { CallbackType.LobbyInvite, typeof( LobbyInvite_t )}, { CallbackType.LobbyEnter, typeof( LobbyEnter_t )}, @@ -279,11 +295,14 @@ namespace Steamworks { CallbackType.SteamShutdown, typeof( SteamShutdown_t )}, { CallbackType.CheckFileSignature, typeof( CheckFileSignature_t )}, { CallbackType.GamepadTextInputDismissed, typeof( GamepadTextInputDismissed_t )}, + { CallbackType.AppResumingFromSuspend, typeof( AppResumingFromSuspend_t )}, + { CallbackType.FloatingGamepadTextInputDismissed, typeof( FloatingGamepadTextInputDismissed_t )}, + { CallbackType.FilterTextDictionaryChanged, typeof( FilterTextDictionaryChanged_t )}, { CallbackType.DlcInstalled, typeof( DlcInstalled_t )}, - { CallbackType.RegisterActivationCodeResponse, typeof( RegisterActivationCodeResponse_t )}, { CallbackType.NewUrlLaunchParameters, typeof( NewUrlLaunchParameters_t )}, { CallbackType.AppProofOfPurchaseKeyResponse, typeof( AppProofOfPurchaseKeyResponse_t )}, { CallbackType.FileDetailsResult, typeof( FileDetailsResult_t )}, + { CallbackType.TimedTrialStatus, typeof( TimedTrialStatus_t )}, { CallbackType.UserStatsReceived, typeof( UserStatsReceived_t )}, { CallbackType.UserStatsStored, typeof( UserStatsStored_t )}, { CallbackType.UserAchievementStored, typeof( UserAchievementStored_t )}, @@ -301,11 +320,10 @@ namespace Steamworks { CallbackType.P2PSessionConnectFail, typeof( P2PSessionConnectFail_t )}, { CallbackType.SteamNetConnectionStatusChangedCallback, typeof( SteamNetConnectionStatusChangedCallback_t )}, { CallbackType.SteamNetAuthenticationStatus, typeof( SteamNetAuthenticationStatus_t )}, + { CallbackType.SteamNetworkingFakeIPResult, typeof( SteamNetworkingFakeIPResult_t )}, + { CallbackType.SteamNetworkingMessagesSessionRequest, typeof( SteamNetworkingMessagesSessionRequest_t )}, + { CallbackType.SteamNetworkingMessagesSessionFailed, typeof( SteamNetworkingMessagesSessionFailed_t )}, { CallbackType.SteamRelayNetworkStatus, typeof( SteamRelayNetworkStatus_t )}, - { CallbackType.RemoteStorageAppSyncedClient, typeof( RemoteStorageAppSyncedClient_t )}, - { CallbackType.RemoteStorageAppSyncedServer, typeof( RemoteStorageAppSyncedServer_t )}, - { CallbackType.RemoteStorageAppSyncProgress, typeof( RemoteStorageAppSyncProgress_t )}, - { CallbackType.RemoteStorageAppSyncStatusCheck, typeof( RemoteStorageAppSyncStatusCheck_t )}, { CallbackType.RemoteStorageFileShareResult, typeof( RemoteStorageFileShareResult_t )}, { CallbackType.RemoteStoragePublishFileResult, typeof( RemoteStoragePublishFileResult_t )}, { CallbackType.RemoteStorageDeletePublishedFileResult, typeof( RemoteStorageDeletePublishedFileResult_t )}, @@ -330,6 +348,7 @@ namespace Steamworks { CallbackType.RemoteStoragePublishedFileUpdated, typeof( RemoteStoragePublishedFileUpdated_t )}, { CallbackType.RemoteStorageFileWriteAsyncComplete, typeof( RemoteStorageFileWriteAsyncComplete_t )}, { CallbackType.RemoteStorageFileReadAsyncComplete, typeof( RemoteStorageFileReadAsyncComplete_t )}, + { CallbackType.RemoteStorageLocalFileChange, typeof( RemoteStorageLocalFileChange_t )}, { CallbackType.GSStatsReceived, typeof( GSStatsReceived_t )}, { CallbackType.GSStatsStored, typeof( GSStatsStored_t )}, { CallbackType.HTTPRequestCompleted, typeof( HTTPRequestCompleted_t )}, @@ -337,6 +356,10 @@ namespace Steamworks { CallbackType.HTTPRequestDataReceived, typeof( HTTPRequestDataReceived_t )}, { CallbackType.ScreenshotReady, typeof( ScreenshotReady_t )}, { CallbackType.ScreenshotRequested, typeof( ScreenshotRequested_t )}, + { CallbackType.SteamInputDeviceConnected, typeof( SteamInputDeviceConnected_t )}, + { CallbackType.SteamInputDeviceDisconnected, typeof( SteamInputDeviceDisconnected_t )}, + { CallbackType.SteamInputConfigurationLoaded, typeof( SteamInputConfigurationLoaded_t )}, + { CallbackType.SteamInputGamepadSlotChange, typeof( SteamInputGamepadSlotChange_t )}, { CallbackType.SteamUGCQueryCompleted, typeof( SteamUGCQueryCompleted_t )}, { CallbackType.SteamUGCRequestUGCDetailsResult, typeof( SteamUGCRequestUGCDetailsResult_t )}, { CallbackType.CreateItemResult, typeof( CreateItemResult_t )}, @@ -354,6 +377,8 @@ namespace Steamworks { CallbackType.RemoveAppDependencyResult, typeof( RemoveAppDependencyResult_t )}, { CallbackType.GetAppDependenciesResult, typeof( GetAppDependenciesResult_t )}, { CallbackType.DeleteItemResult, typeof( DeleteItemResult_t )}, + { CallbackType.UserSubscribedItemsListChanged, typeof( UserSubscribedItemsListChanged_t )}, + { CallbackType.WorkshopEULAStatus, typeof( WorkshopEULAStatus_t )}, { CallbackType.SteamAppInstalled, typeof( SteamAppInstalled_t )}, { CallbackType.SteamAppUninstalled, typeof( SteamAppUninstalled_t )}, { CallbackType.PlaybackStatusHasChanged, typeof( PlaybackStatusHasChanged_t )}, @@ -395,8 +420,6 @@ namespace Steamworks { CallbackType.HTML_UpdateToolTip, typeof( HTML_UpdateToolTip_t )}, { CallbackType.HTML_HideToolTip, typeof( HTML_HideToolTip_t )}, { CallbackType.HTML_BrowserRestarted, typeof( HTML_BrowserRestarted_t )}, - { CallbackType.BroadcastUploadStart, typeof( BroadcastUploadStart_t )}, - { CallbackType.BroadcastUploadStop, typeof( BroadcastUploadStop_t )}, { CallbackType.GetVideoURLResult, typeof( GetVideoURLResult_t )}, { CallbackType.GetOPFSettingsResult, typeof( GetOPFSettingsResult_t )}, { CallbackType.SteamInventoryResultReady, typeof( SteamInventoryResultReady_t )}, @@ -421,6 +444,7 @@ namespace Steamworks { CallbackType.ActiveBeaconsUpdated, typeof( ActiveBeaconsUpdated_t )}, { CallbackType.SteamRemotePlaySessionConnected, typeof( SteamRemotePlaySessionConnected_t )}, { CallbackType.SteamRemotePlaySessionDisconnected, typeof( SteamRemotePlaySessionDisconnected_t )}, + { CallbackType.SteamRemotePlayTogetherGuestInvite, typeof( SteamRemotePlayTogetherGuestInvite_t )}, }; } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs index 84d17aa29..b67457561 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamAppList : SteamInterface + internal unsafe class ISteamAppList : SteamInterface { internal ISteamAppList( bool IsGameServer ) @@ -49,8 +49,7 @@ namespace Steamworks #endregion internal int GetAppName( AppId nAppID, out string pchName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetAppName( Self, nAppID, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -63,8 +62,7 @@ namespace Steamworks #endregion internal int GetAppInstallDir( AppId nAppID, out string pchDirectory ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchDirectory = memory; + using var mempchDirectory = Helpers.TakeMemory(); var returnValue = _GetAppInstallDir( Self, nAppID, mempchDirectory, (1024 * 32) ); pchDirectory = Helpers.MemoryToString( mempchDirectory ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs index 3e5e1c326..a64317fc6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamApps : SteamInterface + internal unsafe class ISteamApps : SteamInterface { internal ISteamApps( bool IsGameServer ) @@ -18,9 +18,6 @@ namespace Steamworks [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamApps_v008", CallingConvention = Platform.CC)] internal static extern IntPtr SteamAPI_SteamApps_v008(); public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamApps_v008(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerApps_v008", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerApps_v008(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerApps_v008(); #region FunctionMeta @@ -159,8 +156,7 @@ namespace Steamworks #endregion internal bool BGetDLCDataByIndex( int iDLC, ref AppId pAppID, [MarshalAs( UnmanagedType.U1 )] ref bool pbAvailable, out string pchName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _BGetDLCDataByIndex( Self, iDLC, ref pAppID, ref pbAvailable, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -204,8 +200,7 @@ namespace Steamworks #endregion internal bool GetCurrentBetaName( out string pchName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetCurrentBetaName( Self, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -241,8 +236,7 @@ namespace Steamworks #endregion internal uint GetAppInstallDir( AppId appID, out string pchFolder ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchFolder = memory; + using var mempchFolder = Helpers.TakeMemory(); var returnValue = _GetAppInstallDir( Self, appID, mempchFolder, (1024 * 32) ); pchFolder = Helpers.MemoryToString( mempchFolder ); return returnValue; @@ -333,8 +327,7 @@ namespace Steamworks #endregion internal int GetLaunchCommandLine( out string pszCommandLine ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszCommandLine = memory; + using var mempszCommandLine = Helpers.TakeMemory(); var returnValue = _GetLaunchCommandLine( Self, mempszCommandLine, (1024 * 32) ); pszCommandLine = Helpers.MemoryToString( mempszCommandLine ); return returnValue; @@ -352,5 +345,29 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamApps_BIsTimedTrial", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BIsTimedTrial( IntPtr self, ref uint punSecondsAllowed, ref uint punSecondsPlayed ); + + #endregion + internal bool BIsTimedTrial( ref uint punSecondsAllowed, ref uint punSecondsPlayed ) + { + var returnValue = _BIsTimedTrial( Self, ref punSecondsAllowed, ref punSecondsPlayed ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamApps_SetDlcContext", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetDlcContext( IntPtr self, AppId nAppID ); + + #endregion + internal bool SetDlcContext( AppId nAppID ) + { + var returnValue = _SetDlcContext( Self, nAppID ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs index 7b8e94540..418f5dab6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamClient : SteamInterface + internal unsafe class ISteamClient : SteamInterface { internal ISteamClient( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs index 4e26a1581..d93dccffe 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamController : SteamInterface + internal unsafe class ISteamController : SteamInterface { internal ISteamController( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamController_v007", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamController_v007(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamController_v007(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamController_v008", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamController_v008(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamController_v008(); #region FunctionMeta diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs index d70f37c2b..b8e5cd443 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamFriends : SteamInterface + internal unsafe class ISteamFriends : SteamInterface { internal ISteamFriends( bool IsGameServer ) @@ -840,5 +840,72 @@ namespace Steamworks _ActivateGameOverlayRemotePlayTogetherInviteDialog( Self, steamIDLobby ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_RegisterProtocolInOverlayBrowser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _RegisterProtocolInOverlayBrowser( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchProtocol ); + + #endregion + internal bool RegisterProtocolInOverlayBrowser( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchProtocol ) + { + var returnValue = _RegisterProtocolInOverlayBrowser( Self, pchProtocol ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_ActivateGameOverlayInviteDialogConnectString", CallingConvention = Platform.CC)] + private static extern void _ActivateGameOverlayInviteDialogConnectString( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchConnectString ); + + #endregion + internal void ActivateGameOverlayInviteDialogConnectString( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchConnectString ) + { + _ActivateGameOverlayInviteDialogConnectString( Self, pchConnectString ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_RequestEquippedProfileItems", CallingConvention = Platform.CC)] + private static extern SteamAPICall_t _RequestEquippedProfileItems( IntPtr self, SteamId steamID ); + + #endregion + internal CallResult RequestEquippedProfileItems( SteamId steamID ) + { + var returnValue = _RequestEquippedProfileItems( Self, steamID ); + return new CallResult( returnValue, IsServer ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_BHasEquippedProfileItem", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BHasEquippedProfileItem( IntPtr self, SteamId steamID, CommunityProfileItemType itemType ); + + #endregion + internal bool BHasEquippedProfileItem( SteamId steamID, CommunityProfileItemType itemType ) + { + var returnValue = _BHasEquippedProfileItem( Self, steamID, itemType ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_GetProfileItemPropertyString", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetProfileItemPropertyString( IntPtr self, SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ); + + #endregion + internal string GetProfileItemPropertyString( SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ) + { + var returnValue = _GetProfileItemPropertyString( Self, steamID, itemType, prop ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_GetProfileItemPropertyUint", CallingConvention = Platform.CC)] + private static extern uint _GetProfileItemPropertyUint( IntPtr self, SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ); + + #endregion + internal uint GetProfileItemPropertyUint( SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ) + { + var returnValue = _GetProfileItemPropertyUint( Self, steamID, itemType, prop ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs index bad5eb31b..3c70b84f7 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamGameSearch : SteamInterface + internal unsafe class ISteamGameSearch : SteamInterface { internal ISteamGameSearch( bool IsGameServer ) @@ -82,8 +82,7 @@ namespace Steamworks #endregion internal GameSearchErrorCode_t RetrieveConnectionDetails( SteamId steamIDHost, out string pchConnectionDetails ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchConnectionDetails = memory; + using var mempchConnectionDetails = Helpers.TakeMemory(); var returnValue = _RetrieveConnectionDetails( Self, steamIDHost, mempchConnectionDetails, (1024 * 32) ); pchConnectionDetails = Helpers.MemoryToString( mempchConnectionDetails ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs index 719af098a..b80a6953c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamGameServer : SteamInterface + internal unsafe class ISteamGameServer : SteamInterface { internal ISteamGameServer( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServer_v013", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServer_v013(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServer_v013(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServer_v015", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServer_v015(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServer_v015(); #region FunctionMeta @@ -258,58 +258,23 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserConnectAndAuthenticate", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _SendUserConnectAndAuthenticate( IntPtr self, uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SetAdvertiseServerActive", CallingConvention = Platform.CC)] + private static extern void _SetAdvertiseServerActive( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bActive ); #endregion - internal bool SendUserConnectAndAuthenticate( uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ) + internal void SetAdvertiseServerActive( [MarshalAs( UnmanagedType.U1 )] bool bActive ) { - var returnValue = _SendUserConnectAndAuthenticate( Self, unIPClient, pvAuthBlob, cubAuthBlobSize, ref pSteamIDUser ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_CreateUnauthenticatedUserConnection", CallingConvention = Platform.CC)] - private static extern SteamId _CreateUnauthenticatedUserConnection( IntPtr self ); - - #endregion - internal SteamId CreateUnauthenticatedUserConnection() - { - var returnValue = _CreateUnauthenticatedUserConnection( Self ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserDisconnect", CallingConvention = Platform.CC)] - private static extern void _SendUserDisconnect( IntPtr self, SteamId steamIDUser ); - - #endregion - internal void SendUserDisconnect( SteamId steamIDUser ) - { - _SendUserDisconnect( Self, steamIDUser ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_BUpdateUserData", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _BUpdateUserData( IntPtr self, SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ); - - #endregion - internal bool BUpdateUserData( SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ) - { - var returnValue = _BUpdateUserData( Self, steamIDUser, pchPlayerName, uScore ); - return returnValue; + _SetAdvertiseServerActive( Self, bActive ); } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_GetAuthSessionTicket", CallingConvention = Platform.CC)] - private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ); + private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSnid ); #endregion - internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ) + internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSnid ) { - var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket ); + var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket, ref pSnid ); return returnValue; } @@ -422,36 +387,6 @@ namespace Steamworks return returnValue; } - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_EnableHeartbeats", CallingConvention = Platform.CC)] - private static extern void _EnableHeartbeats( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bActive ); - - #endregion - internal void EnableHeartbeats( [MarshalAs( UnmanagedType.U1 )] bool bActive ) - { - _EnableHeartbeats( Self, bActive ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SetHeartbeatInterval", CallingConvention = Platform.CC)] - private static extern void _SetHeartbeatInterval( IntPtr self, int iHeartbeatInterval ); - - #endregion - internal void SetHeartbeatInterval( int iHeartbeatInterval ) - { - _SetHeartbeatInterval( Self, iHeartbeatInterval ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_ForceHeartbeat", CallingConvention = Platform.CC)] - private static extern void _ForceHeartbeat( IntPtr self ); - - #endregion - internal void ForceHeartbeat() - { - _ForceHeartbeat( Self ); - } - #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_AssociateWithClan", CallingConvention = Platform.CC)] private static extern SteamAPICall_t _AssociateWithClan( IntPtr self, SteamId steamIDClan ); @@ -474,5 +409,50 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserConnectAndAuthenticate_DEPRECATED", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SendUserConnectAndAuthenticate_DEPRECATED( IntPtr self, uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ); + + #endregion + internal bool SendUserConnectAndAuthenticate_DEPRECATED( uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ) + { + var returnValue = _SendUserConnectAndAuthenticate_DEPRECATED( Self, unIPClient, pvAuthBlob, cubAuthBlobSize, ref pSteamIDUser ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_CreateUnauthenticatedUserConnection", CallingConvention = Platform.CC)] + private static extern SteamId _CreateUnauthenticatedUserConnection( IntPtr self ); + + #endregion + internal SteamId CreateUnauthenticatedUserConnection() + { + var returnValue = _CreateUnauthenticatedUserConnection( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserDisconnect_DEPRECATED", CallingConvention = Platform.CC)] + private static extern void _SendUserDisconnect_DEPRECATED( IntPtr self, SteamId steamIDUser ); + + #endregion + internal void SendUserDisconnect_DEPRECATED( SteamId steamIDUser ) + { + _SendUserDisconnect_DEPRECATED( Self, steamIDUser ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_BUpdateUserData", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BUpdateUserData( IntPtr self, SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ); + + #endregion + internal bool BUpdateUserData( SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ) + { + var returnValue = _BUpdateUserData( Self, steamIDUser, pchPlayerName, uScore ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs index fee875705..de293cbe5 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamGameServerStats : SteamInterface + internal unsafe class ISteamGameServerStats : SteamInterface { internal ISteamGameServerStats( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs index e9bc08dd9..38c6b462e 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamHTMLSurface : SteamInterface + internal unsafe class ISteamHTMLSurface : SteamInterface { internal ISteamHTMLSurface( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs index 3d1d7b148..599054b5c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamHTTP : SteamInterface + internal unsafe class ISteamHTTP : SteamInterface { internal ISteamHTTP( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs index 5a1efd81f..3d1ab5227 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamInput : SteamInterface + internal unsafe class ISteamInput : SteamInterface { internal ISteamInput( bool IsGameServer ) @@ -15,20 +15,20 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamInput_v001", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamInput_v001(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamInput_v001(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamInput_v006", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamInput_v006(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamInput_v006(); #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_Init", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _Init( IntPtr self ); + private static extern bool _Init( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bExplicitlyCallRunFrame ); #endregion - internal bool Init() + internal bool Init( [MarshalAs( UnmanagedType.U1 )] bool bExplicitlyCallRunFrame ) { - var returnValue = _Init( Self ); + var returnValue = _Init( Self, bExplicitlyCallRunFrame ); return returnValue; } @@ -45,13 +45,49 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_RunFrame", CallingConvention = Platform.CC)] - private static extern void _RunFrame( IntPtr self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_SetInputActionManifestFilePath", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetInputActionManifestFilePath( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputActionManifestAbsolutePath ); #endregion - internal void RunFrame() + internal bool SetInputActionManifestFilePath( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputActionManifestAbsolutePath ) { - _RunFrame( Self ); + var returnValue = _SetInputActionManifestFilePath( Self, pchInputActionManifestAbsolutePath ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_RunFrame", CallingConvention = Platform.CC)] + private static extern void _RunFrame( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bReservedValue ); + + #endregion + internal void RunFrame( [MarshalAs( UnmanagedType.U1 )] bool bReservedValue ) + { + _RunFrame( Self, bReservedValue ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_BWaitForData", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BWaitForData( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bWaitForever, uint unTimeout ); + + #endregion + internal bool BWaitForData( [MarshalAs( UnmanagedType.U1 )] bool bWaitForever, uint unTimeout ) + { + var returnValue = _BWaitForData( Self, bWaitForever, unTimeout ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_BNewDataAvailable", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BNewDataAvailable( IntPtr self ); + + #endregion + internal bool BNewDataAvailable() + { + var returnValue = _BNewDataAvailable( Self ); + return returnValue; } #region FunctionMeta @@ -65,6 +101,16 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_EnableDeviceCallbacks", CallingConvention = Platform.CC)] + private static extern void _EnableDeviceCallbacks( IntPtr self ); + + #endregion + internal void EnableDeviceCallbacks() + { + _EnableDeviceCallbacks( Self ); + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetActionSetHandle", CallingConvention = Platform.CC)] private static extern InputActionSetHandle_t _GetActionSetHandle( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pszActionSetName ); @@ -171,6 +217,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetStringForDigitalActionName", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetStringForDigitalActionName( IntPtr self, InputDigitalActionHandle_t eActionHandle ); + + #endregion + internal string GetStringForDigitalActionName( InputDigitalActionHandle_t eActionHandle ) + { + var returnValue = _GetStringForDigitalActionName( Self, eActionHandle ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetAnalogActionHandle", CallingConvention = Platform.CC)] private static extern InputAnalogActionHandle_t _GetAnalogActionHandle( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pszActionName ); @@ -205,13 +262,35 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphForActionOrigin", CallingConvention = Platform.CC)] - private static extern Utf8StringPointer _GetGlyphForActionOrigin( IntPtr self, InputActionOrigin eOrigin ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphPNGForActionOrigin", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetGlyphPNGForActionOrigin( IntPtr self, InputActionOrigin eOrigin, GlyphSize eSize, uint unFlags ); #endregion - internal string GetGlyphForActionOrigin( InputActionOrigin eOrigin ) + internal string GetGlyphPNGForActionOrigin( InputActionOrigin eOrigin, GlyphSize eSize, uint unFlags ) { - var returnValue = _GetGlyphForActionOrigin( Self, eOrigin ); + var returnValue = _GetGlyphPNGForActionOrigin( Self, eOrigin, eSize, unFlags ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphSVGForActionOrigin", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetGlyphSVGForActionOrigin( IntPtr self, InputActionOrigin eOrigin, uint unFlags ); + + #endregion + internal string GetGlyphSVGForActionOrigin( InputActionOrigin eOrigin, uint unFlags ) + { + var returnValue = _GetGlyphSVGForActionOrigin( Self, eOrigin, unFlags ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphForActionOrigin_Legacy", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetGlyphForActionOrigin_Legacy( IntPtr self, InputActionOrigin eOrigin ); + + #endregion + internal string GetGlyphForActionOrigin_Legacy( InputActionOrigin eOrigin ) + { + var returnValue = _GetGlyphForActionOrigin_Legacy( Self, eOrigin ); return returnValue; } @@ -226,6 +305,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetStringForAnalogActionName", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetStringForAnalogActionName( IntPtr self, InputAnalogActionHandle_t eActionHandle ); + + #endregion + internal string GetStringForAnalogActionName( InputAnalogActionHandle_t eActionHandle ) + { + var returnValue = _GetStringForAnalogActionName( Self, eActionHandle ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_StopAnalogActionMomentum", CallingConvention = Platform.CC)] private static extern void _StopAnalogActionMomentum( IntPtr self, InputHandle_t inputHandle, InputAnalogActionHandle_t eAction ); @@ -257,6 +347,26 @@ namespace Steamworks _TriggerVibration( Self, inputHandle, usLeftSpeed, usRightSpeed ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerVibrationExtended", CallingConvention = Platform.CC)] + private static extern void _TriggerVibrationExtended( IntPtr self, InputHandle_t inputHandle, ushort usLeftSpeed, ushort usRightSpeed, ushort usLeftTriggerSpeed, ushort usRightTriggerSpeed ); + + #endregion + internal void TriggerVibrationExtended( InputHandle_t inputHandle, ushort usLeftSpeed, ushort usRightSpeed, ushort usLeftTriggerSpeed, ushort usRightTriggerSpeed ) + { + _TriggerVibrationExtended( Self, inputHandle, usLeftSpeed, usRightSpeed, usLeftTriggerSpeed, usRightTriggerSpeed ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerSimpleHapticEvent", CallingConvention = Platform.CC)] + private static extern void _TriggerSimpleHapticEvent( IntPtr self, InputHandle_t inputHandle, ControllerHapticLocation eHapticLocation, byte nIntensity, char nGainDB, byte nOtherIntensity, char nOtherGainDB ); + + #endregion + internal void TriggerSimpleHapticEvent( InputHandle_t inputHandle, ControllerHapticLocation eHapticLocation, byte nIntensity, char nGainDB, byte nOtherIntensity, char nOtherGainDB ) + { + _TriggerSimpleHapticEvent( Self, inputHandle, eHapticLocation, nIntensity, nGainDB, nOtherIntensity, nOtherGainDB ); + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_SetLEDColor", CallingConvention = Platform.CC)] private static extern void _SetLEDColor( IntPtr self, InputHandle_t inputHandle, byte nColorR, byte nColorG, byte nColorB, uint nFlags ); @@ -268,23 +378,23 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerHapticPulse", CallingConvention = Platform.CC)] - private static extern void _TriggerHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_Legacy_TriggerHapticPulse", CallingConvention = Platform.CC)] + private static extern void _Legacy_TriggerHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ); #endregion - internal void TriggerHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ) + internal void Legacy_TriggerHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ) { - _TriggerHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec ); + _Legacy_TriggerHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec ); } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerRepeatedHapticPulse", CallingConvention = Platform.CC)] - private static extern void _TriggerRepeatedHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_Legacy_TriggerRepeatedHapticPulse", CallingConvention = Platform.CC)] + private static extern void _Legacy_TriggerRepeatedHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ); #endregion - internal void TriggerRepeatedHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ) + internal void Legacy_TriggerRepeatedHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ) { - _TriggerRepeatedHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec, usOffMicroSec, unRepeat, nFlags ); + _Legacy_TriggerRepeatedHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec, usOffMicroSec, unRepeat, nFlags ); } #region FunctionMeta @@ -399,5 +509,16 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetSessionInputConfigurationSettings", CallingConvention = Platform.CC)] + private static extern ushort _GetSessionInputConfigurationSettings( IntPtr self ); + + #endregion + internal ushort GetSessionInputConfigurationSettings() + { + var returnValue = _GetSessionInputConfigurationSettings( Self ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs index b8c24ac7d..5b4d20160 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamInventory : SteamInterface + internal unsafe class ISteamInventory : SteamInterface { internal ISteamInventory( bool IsGameServer ) @@ -28,9 +28,6 @@ namespace Steamworks private static extern Result _GetResultStatus( IntPtr self, SteamInventoryResult_t resultHandle ); #endregion - /// - /// Find out the status of an asynchronous inventory result handle. - /// internal Result GetResultStatus( SteamInventoryResult_t resultHandle ) { var returnValue = _GetResultStatus( Self, resultHandle ); @@ -43,9 +40,6 @@ namespace Steamworks private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ); #endregion - /// - /// Copies the contents of a result set into a flat array. The specific contents of the result set depend on which query which was used. - /// internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ) { var returnValue = _GetResultItems( Self, resultHandle, pOutItemsArray, ref punOutItemsArraySize ); @@ -60,8 +54,7 @@ namespace Steamworks #endregion internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchValueBuffer = memory; + using var mempchValueBuffer = Helpers.TakeMemory(); var returnValue = _GetResultItemProperty( Self, resultHandle, unItemIndex, pchPropertyName, mempchValueBuffer, ref punValueBufferSizeOut ); pchValueBuffer = Helpers.MemoryToString( mempchValueBuffer ); return returnValue; @@ -72,9 +65,6 @@ namespace Steamworks private static extern uint _GetResultTimestamp( IntPtr self, SteamInventoryResult_t resultHandle ); #endregion - /// - /// Returns the server time at which the result was generated. Compare against the value of IClientUtils::GetServerRealTime() to determine age. - /// internal uint GetResultTimestamp( SteamInventoryResult_t resultHandle ) { var returnValue = _GetResultTimestamp( Self, resultHandle ); @@ -87,9 +77,6 @@ namespace Steamworks private static extern bool _CheckResultSteamID( IntPtr self, SteamInventoryResult_t resultHandle, SteamId steamIDExpected ); #endregion - /// - /// Returns true if the result belongs to the target steam ID or false if the result does not. This is important when using DeserializeResult to verify that a remote player is not pretending to have a different users inventory. - /// internal bool CheckResultSteamID( SteamInventoryResult_t resultHandle, SteamId steamIDExpected ) { var returnValue = _CheckResultSteamID( Self, resultHandle, steamIDExpected ); @@ -101,9 +88,6 @@ namespace Steamworks private static extern void _DestroyResult( IntPtr self, SteamInventoryResult_t resultHandle ); #endregion - /// - /// Destroys a result handle and frees all associated memory. - /// internal void DestroyResult( SteamInventoryResult_t resultHandle ) { _DestroyResult( Self, resultHandle ); @@ -115,9 +99,6 @@ namespace Steamworks private static extern bool _GetAllItems( IntPtr self, ref SteamInventoryResult_t pResultHandle ); #endregion - /// - /// Captures the entire state of the current users Steam inventory. - /// internal bool GetAllItems( ref SteamInventoryResult_t pResultHandle ) { var returnValue = _GetAllItems( Self, ref pResultHandle ); @@ -130,9 +111,6 @@ namespace Steamworks private static extern bool _GetItemsByID( IntPtr self, ref SteamInventoryResult_t pResultHandle, ref InventoryItemId pInstanceIDs, uint unCountInstanceIDs ); #endregion - /// - /// Captures the state of a subset of the current users Steam inventory identified by an array of item instance IDs. - /// internal bool GetItemsByID( ref SteamInventoryResult_t pResultHandle, ref InventoryItemId pInstanceIDs, uint unCountInstanceIDs ) { var returnValue = _GetItemsByID( Self, ref pResultHandle, ref pInstanceIDs, unCountInstanceIDs ); @@ -181,9 +159,6 @@ namespace Steamworks private static extern bool _GrantPromoItems( IntPtr self, ref SteamInventoryResult_t pResultHandle ); #endregion - /// - /// GrantPromoItems() checks the list of promotional items for which the user may be eligible and grants the items (one time only). - /// internal bool GrantPromoItems( ref SteamInventoryResult_t pResultHandle ) { var returnValue = _GrantPromoItems( Self, ref pResultHandle ); @@ -220,9 +195,6 @@ namespace Steamworks private static extern bool _ConsumeItem( IntPtr self, ref SteamInventoryResult_t pResultHandle, InventoryItemId itemConsume, uint unQuantity ); #endregion - /// - /// ConsumeItem() removes items from the inventory permanently. - /// internal bool ConsumeItem( ref SteamInventoryResult_t pResultHandle, InventoryItemId itemConsume, uint unQuantity ) { var returnValue = _ConsumeItem( Self, ref pResultHandle, itemConsume, unQuantity ); @@ -258,9 +230,6 @@ namespace Steamworks private static extern void _SendItemDropHeartbeat( IntPtr self ); #endregion - /// - /// Deprecated method. Playtime accounting is performed on the Steam servers. - /// internal void SendItemDropHeartbeat() { _SendItemDropHeartbeat( Self ); @@ -272,9 +241,6 @@ namespace Steamworks private static extern bool _TriggerItemDrop( IntPtr self, ref SteamInventoryResult_t pResultHandle, InventoryDefId dropListDefinition ); #endregion - /// - /// Playtime credit must be consumed and turned into item drops by your game. - /// internal bool TriggerItemDrop( ref SteamInventoryResult_t pResultHandle, InventoryDefId dropListDefinition ) { var returnValue = _TriggerItemDrop( Self, ref pResultHandle, dropListDefinition ); @@ -299,9 +265,6 @@ namespace Steamworks private static extern bool _LoadItemDefinitions( IntPtr self ); #endregion - /// - /// LoadItemDefinitions triggers the automatic load and refresh of item definitions. - /// internal bool LoadItemDefinitions() { var returnValue = _LoadItemDefinitions( Self ); @@ -328,8 +291,7 @@ namespace Steamworks #endregion internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchValueBuffer = memory; + using var mempchValueBuffer = Helpers.TakeMemory(); var returnValue = _GetItemDefinitionProperty( Self, iDefinition, pchPropertyName, mempchValueBuffer, ref punValueBufferSizeOut ); pchValueBuffer = Helpers.MemoryToString( mempchValueBuffer ); return returnValue; @@ -498,5 +460,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_InspectItem", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _InspectItem( IntPtr self, ref SteamInventoryResult_t pResultHandle, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchItemToken ); + + #endregion + internal bool InspectItem( ref SteamInventoryResult_t pResultHandle, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchItemToken ) + { + var returnValue = _InspectItem( Self, ref pResultHandle, pchItemToken ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs index 593676092..801d41086 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmaking : SteamInterface + internal unsafe class ISteamMatchmaking : SteamInterface { internal ISteamMatchmaking( bool IsGameServer ) @@ -266,10 +266,8 @@ namespace Steamworks #endregion internal bool GetLobbyDataByIndex( SteamId steamIDLobby, int iLobbyData, out string pchKey, out string pchValue ) { - using var memoryKey = Helpers.TakeMemory(); - using var memoryValue = Helpers.TakeMemory(); - IntPtr mempchKey = memoryKey; - IntPtr mempchValue = memoryValue; + using var mempchKey = Helpers.TakeMemory(); + using var mempchValue = Helpers.TakeMemory(); var returnValue = _GetLobbyDataByIndex( Self, steamIDLobby, iLobbyData, mempchKey, (1024 * 32), mempchValue, (1024 * 32) ); pchKey = Helpers.MemoryToString( mempchKey ); pchValue = Helpers.MemoryToString( mempchValue ); diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs index 9f14a9618..0306b76f6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingPingResponse : SteamInterface + internal unsafe class ISteamMatchmakingPingResponse : SteamInterface { internal ISteamMatchmakingPingResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs index ce4f02aee..a5ed92691 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingPlayersResponse : SteamInterface + internal unsafe class ISteamMatchmakingPlayersResponse : SteamInterface { internal ISteamMatchmakingPlayersResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs index 7367634d1..78507b9ea 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingRulesResponse : SteamInterface + internal unsafe class ISteamMatchmakingRulesResponse : SteamInterface { internal ISteamMatchmakingRulesResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs index 9ab74dc67..8fd0a9e98 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingServerListResponse : SteamInterface + internal unsafe class ISteamMatchmakingServerListResponse : SteamInterface { internal ISteamMatchmakingServerListResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs index bd7bb81ba..3eb098c00 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingServers : SteamInterface + internal unsafe class ISteamMatchmakingServers : SteamInterface { internal ISteamMatchmakingServers( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs index 1ed2f6707..6534a83c0 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMusic : SteamInterface + internal unsafe class ISteamMusic : SteamInterface { internal ISteamMusic( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs index 620b7c69f..cf7350288 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMusicRemote : SteamInterface + internal unsafe class ISteamMusicRemote : SteamInterface { internal ISteamMusicRemote( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs index baef24dd3..f14955788 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamNetworking : SteamInterface + internal unsafe class ISteamNetworking : SteamInterface { internal ISteamNetworking( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs deleted file mode 100644 index 3379820af..000000000 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Steamworks.Data; - - -namespace Steamworks -{ - internal class ISteamNetworkingConnectionCustomSignaling : SteamInterface - { - - internal ISteamNetworkingConnectionCustomSignaling( bool IsGameServer ) - { - SetupInterface( IsGameServer ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingConnectionCustomSignaling_SendSignal", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _SendSignal( IntPtr self, Connection hConn, ref ConnectionInfo info, IntPtr pMsg, int cbMsg ); - - #endregion - internal bool SendSignal( Connection hConn, ref ConnectionInfo info, IntPtr pMsg, int cbMsg ) - { - var returnValue = _SendSignal( Self, hConn, ref info, pMsg, cbMsg ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingConnectionCustomSignaling_Release", CallingConvention = Platform.CC)] - private static extern void _Release( IntPtr self ); - - #endregion - internal void Release() - { - _Release( Self ); - } - - } -} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs deleted file mode 100644 index 670919571..000000000 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Steamworks.Data; - - -namespace Steamworks -{ - internal class ISteamNetworkingCustomSignalingRecvContext : SteamInterface - { - - internal ISteamNetworkingCustomSignalingRecvContext( bool IsGameServer ) - { - SetupInterface( IsGameServer ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingCustomSignalingRecvContext_OnConnectRequest", CallingConvention = Platform.CC)] - private static extern IntPtr _OnConnectRequest( IntPtr self, Connection hConn, ref NetIdentity identityPeer ); - - #endregion - internal IntPtr OnConnectRequest( Connection hConn, ref NetIdentity identityPeer ) - { - var returnValue = _OnConnectRequest( Self, hConn, ref identityPeer ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingCustomSignalingRecvContext_SendRejectionSignal", CallingConvention = Platform.CC)] - private static extern void _SendRejectionSignal( IntPtr self, ref NetIdentity identityPeer, IntPtr pMsg, int cbMsg ); - - #endregion - internal void SendRejectionSignal( ref NetIdentity identityPeer, IntPtr pMsg, int cbMsg ) - { - _SendRejectionSignal( Self, ref identityPeer, pMsg, cbMsg ); - } - - } -} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs new file mode 100644 index 000000000..776ab8a68 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Steamworks.Data; + + +namespace Steamworks +{ + internal unsafe class ISteamNetworkingFakeUDPPort : SteamInterface + { + + internal ISteamNetworkingFakeUDPPort( bool IsGameServer ) + { + SetupInterface( IsGameServer ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_DestroyFakeUDPPort", CallingConvention = Platform.CC)] + private static extern void _DestroyFakeUDPPort( IntPtr self ); + + #endregion + internal void DestroyFakeUDPPort() + { + _DestroyFakeUDPPort( Self ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_SendMessageToFakeIP", CallingConvention = Platform.CC)] + private static extern Result _SendMessageToFakeIP( IntPtr self, ref NetAddress remoteAddress, IntPtr pData, uint cbData, int nSendFlags ); + + #endregion + internal Result SendMessageToFakeIP( ref NetAddress remoteAddress, IntPtr pData, uint cbData, int nSendFlags ) + { + var returnValue = _SendMessageToFakeIP( Self, ref remoteAddress, pData, cbData, nSendFlags ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_ReceiveMessages", CallingConvention = Platform.CC)] + private static extern int _ReceiveMessages( IntPtr self, IntPtr ppOutMessages, int nMaxMessages ); + + #endregion + internal int ReceiveMessages( IntPtr ppOutMessages, int nMaxMessages ) + { + var returnValue = _ReceiveMessages( Self, ppOutMessages, nMaxMessages ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_ScheduleCleanup", CallingConvention = Platform.CC)] + private static extern void _ScheduleCleanup( IntPtr self, ref NetAddress remoteAddress ); + + #endregion + internal void ScheduleCleanup( ref NetAddress remoteAddress ) + { + _ScheduleCleanup( Self, ref remoteAddress ); + } + + } +} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs new file mode 100644 index 000000000..715c34a13 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs @@ -0,0 +1,96 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Steamworks.Data; + + +namespace Steamworks +{ + internal unsafe class ISteamNetworkingMessages : SteamInterface + { + + internal ISteamNetworkingMessages( bool IsGameServer ) + { + SetupInterface( IsGameServer ); + } + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingMessages_SteamAPI_v002", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamNetworkingMessages_SteamAPI_v002(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamNetworkingMessages_SteamAPI_v002(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerNetworkingMessages_SteamAPI_v002", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerNetworkingMessages_SteamAPI_v002(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerNetworkingMessages_SteamAPI_v002(); + + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_SendMessageToUser", CallingConvention = Platform.CC)] + private static extern Result _SendMessageToUser( IntPtr self, ref NetIdentity identityRemote, [In,Out] IntPtr[] pubData, uint cubData, int nSendFlags, int nRemoteChannel ); + + #endregion + internal Result SendMessageToUser( ref NetIdentity identityRemote, [In,Out] IntPtr[] pubData, uint cubData, int nSendFlags, int nRemoteChannel ) + { + var returnValue = _SendMessageToUser( Self, ref identityRemote, pubData, cubData, nSendFlags, nRemoteChannel ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_ReceiveMessagesOnChannel", CallingConvention = Platform.CC)] + private static extern int _ReceiveMessagesOnChannel( IntPtr self, int nLocalChannel, IntPtr ppOutMessages, int nMaxMessages ); + + #endregion + internal int ReceiveMessagesOnChannel( int nLocalChannel, IntPtr ppOutMessages, int nMaxMessages ) + { + var returnValue = _ReceiveMessagesOnChannel( Self, nLocalChannel, ppOutMessages, nMaxMessages ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_AcceptSessionWithUser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _AcceptSessionWithUser( IntPtr self, ref NetIdentity identityRemote ); + + #endregion + internal bool AcceptSessionWithUser( ref NetIdentity identityRemote ) + { + var returnValue = _AcceptSessionWithUser( Self, ref identityRemote ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_CloseSessionWithUser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _CloseSessionWithUser( IntPtr self, ref NetIdentity identityRemote ); + + #endregion + internal bool CloseSessionWithUser( ref NetIdentity identityRemote ) + { + var returnValue = _CloseSessionWithUser( Self, ref identityRemote ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_CloseChannelWithUser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _CloseChannelWithUser( IntPtr self, ref NetIdentity identityRemote, int nLocalChannel ); + + #endregion + internal bool CloseChannelWithUser( ref NetIdentity identityRemote, int nLocalChannel ) + { + var returnValue = _CloseChannelWithUser( Self, ref identityRemote, nLocalChannel ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_GetSessionConnectionInfo", CallingConvention = Platform.CC)] + private static extern ConnectionState _GetSessionConnectionInfo( IntPtr self, ref NetIdentity identityRemote, ref ConnectionInfo pConnectionInfo, ref ConnectionStatus pQuickStatus ); + + #endregion + internal ConnectionState GetSessionConnectionInfo( ref NetIdentity identityRemote, ref ConnectionInfo pConnectionInfo, ref ConnectionStatus pQuickStatus ) + { + var returnValue = _GetSessionConnectionInfo( Self, ref identityRemote, ref pConnectionInfo, ref pQuickStatus ); + return returnValue; + } + + } +} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs index 61190cd7b..becc5d302 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamNetworkingSockets : SteamInterface + internal unsafe class ISteamNetworkingSockets : SteamInterface { internal ISteamNetworkingSockets( bool IsGameServer ) @@ -15,12 +15,12 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingSockets_v008", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamNetworkingSockets_v008(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamNetworkingSockets_v008(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerNetworkingSockets_v008", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerNetworkingSockets_v008(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerNetworkingSockets_v008(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingSockets_SteamAPI_v012", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamNetworkingSockets_SteamAPI_v012(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamNetworkingSockets_SteamAPI_v012(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v012", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v012(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v012(); #region FunctionMeta @@ -47,23 +47,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateListenSocketP2P", CallingConvention = Platform.CC)] - private static extern Socket _CreateListenSocketP2P( IntPtr self, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Socket _CreateListenSocketP2P( IntPtr self, int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Socket CreateListenSocketP2P( int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Socket CreateListenSocketP2P( int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _CreateListenSocketP2P( Self, nVirtualPort, nOptions, pOptions ); + var returnValue = _CreateListenSocketP2P( Self, nLocalVirtualPort, nOptions, pOptions ); return returnValue; } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConnectP2P", CallingConvention = Platform.CC)] - private static extern Connection _ConnectP2P( IntPtr self, ref NetIdentity identityRemote, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Connection _ConnectP2P( IntPtr self, ref NetIdentity identityRemote, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Connection ConnectP2P( ref NetIdentity identityRemote, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Connection ConnectP2P( ref NetIdentity identityRemote, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _ConnectP2P( Self, ref identityRemote, nVirtualPort, nOptions, pOptions ); + var returnValue = _ConnectP2P( Self, ref identityRemote, nRemoteVirtualPort, nOptions, pOptions ); return returnValue; } @@ -143,8 +143,7 @@ namespace Steamworks #endregion internal bool GetConnectionName( Connection hPeer, out string pszName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszName = memory; + using var mempszName = Helpers.TakeMemory(); var returnValue = _GetConnectionName( Self, hPeer, mempszName, (1024 * 32) ); pszName = Helpers.MemoryToString( mempszName ); return returnValue; @@ -163,12 +162,12 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_SendMessages", CallingConvention = Platform.CC)] - private static extern void _SendMessages( IntPtr self, int nMessages, ref NetMsg pMessages, [In,Out] long[] pOutMessageNumberOrResult ); + private static extern void _SendMessages( IntPtr self, int nMessages, NetMsg** pMessages, long* pOutMessageNumberOrResult ); #endregion - internal void SendMessages( int nMessages, ref NetMsg pMessages, [In,Out] long[] pOutMessageNumberOrResult ) + internal void SendMessages( int nMessages, NetMsg** pMessages, long* pOutMessageNumberOrResult ) { - _SendMessages( Self, nMessages, ref pMessages, pOutMessageNumberOrResult ); + _SendMessages( Self, nMessages, pMessages, pOutMessageNumberOrResult ); } #region FunctionMeta @@ -206,14 +205,13 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetQuickConnectionStatus", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetQuickConnectionStatus( IntPtr self, Connection hConn, ref SteamNetworkingQuickConnectionStatus pStats ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetConnectionRealTimeStatus", CallingConvention = Platform.CC)] + private static extern Result _GetConnectionRealTimeStatus( IntPtr self, Connection hConn, ref ConnectionStatus pStatus, int nLanes, [In,Out] ConnectionLaneStatus[]? pLanes ); #endregion - internal bool GetQuickConnectionStatus( Connection hConn, ref SteamNetworkingQuickConnectionStatus pStats ) + internal Result GetConnectionRealTimeStatus( Connection hConn, ref ConnectionStatus pStatus, int nLanes, [In,Out] ConnectionLaneStatus[]? pLanes ) { - var returnValue = _GetQuickConnectionStatus( Self, hConn, ref pStats ); + var returnValue = _GetConnectionRealTimeStatus( Self, hConn, ref pStatus, nLanes, pLanes ); return returnValue; } @@ -224,8 +222,7 @@ namespace Steamworks #endregion internal int GetDetailedConnectionStatus( Connection hConn, out string pszBuf ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszBuf = memory; + using var mempszBuf = Helpers.TakeMemory(); var returnValue = _GetDetailedConnectionStatus( Self, hConn, mempszBuf, (1024 * 32) ); pszBuf = Helpers.MemoryToString( mempszBuf ); return returnValue; @@ -255,6 +252,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConfigureConnectionLanes", CallingConvention = Platform.CC)] + private static extern Result _ConfigureConnectionLanes( IntPtr self, Connection hConn, int nNumLanes, [In,Out] int[] pLanePriorities, [In,Out] ushort[] pLaneWeights ); + + #endregion + internal Result ConfigureConnectionLanes( Connection hConn, int nNumLanes, [In,Out] int[] pLanePriorities, [In,Out] ushort[] pLaneWeights ) + { + var returnValue = _ConfigureConnectionLanes( Self, hConn, nNumLanes, pLanePriorities, pLaneWeights ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetIdentity", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -349,23 +357,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_FindRelayAuthTicketForServer", CallingConvention = Platform.CC)] - private static extern int _FindRelayAuthTicketForServer( IntPtr self, ref NetIdentity identityGameServer, int nVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ); + private static extern int _FindRelayAuthTicketForServer( IntPtr self, ref NetIdentity identityGameServer, int nRemoteVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ); #endregion - internal int FindRelayAuthTicketForServer( ref NetIdentity identityGameServer, int nVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ) + internal int FindRelayAuthTicketForServer( ref NetIdentity identityGameServer, int nRemoteVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ) { - var returnValue = _FindRelayAuthTicketForServer( Self, ref identityGameServer, nVirtualPort, pOutParsedTicket ); + var returnValue = _FindRelayAuthTicketForServer( Self, ref identityGameServer, nRemoteVirtualPort, pOutParsedTicket ); return returnValue; } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConnectToHostedDedicatedServer", CallingConvention = Platform.CC)] - private static extern Connection _ConnectToHostedDedicatedServer( IntPtr self, ref NetIdentity identityTarget, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Connection _ConnectToHostedDedicatedServer( IntPtr self, ref NetIdentity identityTarget, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Connection ConnectToHostedDedicatedServer( ref NetIdentity identityTarget, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Connection ConnectToHostedDedicatedServer( ref NetIdentity identityTarget, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _ConnectToHostedDedicatedServer( Self, ref identityTarget, nVirtualPort, nOptions, pOptions ); + var returnValue = _ConnectToHostedDedicatedServer( Self, ref identityTarget, nRemoteVirtualPort, nOptions, pOptions ); return returnValue; } @@ -404,12 +412,12 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateHostedDedicatedServerListenSocket", CallingConvention = Platform.CC)] - private static extern Socket _CreateHostedDedicatedServerListenSocket( IntPtr self, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Socket _CreateHostedDedicatedServerListenSocket( IntPtr self, int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Socket CreateHostedDedicatedServerListenSocket( int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Socket CreateHostedDedicatedServerListenSocket( int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _CreateHostedDedicatedServerListenSocket( Self, nVirtualPort, nOptions, pOptions ); + var returnValue = _CreateHostedDedicatedServerListenSocket( Self, nLocalVirtualPort, nOptions, pOptions ); return returnValue; } @@ -426,12 +434,12 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConnectP2PCustomSignaling", CallingConvention = Platform.CC)] - private static extern Connection _ConnectP2PCustomSignaling( IntPtr self, IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Connection _ConnectP2PCustomSignaling( IntPtr self, IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Connection ConnectP2PCustomSignaling( IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Connection ConnectP2PCustomSignaling( IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _ConnectP2PCustomSignaling( Self, pSignaling, ref pPeerIdentity, nOptions, pOptions ); + var returnValue = _ConnectP2PCustomSignaling( Self, pSignaling, ref pPeerIdentity, nRemoteVirtualPort, nOptions, pOptions ); return returnValue; } @@ -471,5 +479,80 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ResetIdentity", CallingConvention = Platform.CC)] + private static extern void _ResetIdentity( IntPtr self, ref NetIdentity pIdentity ); + + #endregion + internal void ResetIdentity( ref NetIdentity pIdentity ) + { + _ResetIdentity( Self, ref pIdentity ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_RunCallbacks", CallingConvention = Platform.CC)] + private static extern void _RunCallbacks( IntPtr self ); + + #endregion + internal void RunCallbacks() + { + _RunCallbacks( Self ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_BeginAsyncRequestFakeIP", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BeginAsyncRequestFakeIP( IntPtr self, int nNumPorts ); + + #endregion + internal bool BeginAsyncRequestFakeIP( int nNumPorts ) + { + var returnValue = _BeginAsyncRequestFakeIP( Self, nNumPorts ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetFakeIP", CallingConvention = Platform.CC)] + private static extern void _GetFakeIP( IntPtr self, int idxFirstPort, ref SteamNetworkingFakeIPResult_t pInfo ); + + #endregion + internal void GetFakeIP( int idxFirstPort, ref SteamNetworkingFakeIPResult_t pInfo ) + { + _GetFakeIP( Self, idxFirstPort, ref pInfo ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateListenSocketP2PFakeIP", CallingConvention = Platform.CC)] + private static extern Socket _CreateListenSocketP2PFakeIP( IntPtr self, int idxFakePort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + + #endregion + internal Socket CreateListenSocketP2PFakeIP( int idxFakePort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + { + var returnValue = _CreateListenSocketP2PFakeIP( Self, idxFakePort, nOptions, pOptions ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetRemoteFakeIPForConnection", CallingConvention = Platform.CC)] + private static extern Result _GetRemoteFakeIPForConnection( IntPtr self, Connection hConn, [In,Out] NetAddress[] pOutAddr ); + + #endregion + internal Result GetRemoteFakeIPForConnection( Connection hConn, [In,Out] NetAddress[] pOutAddr ) + { + var returnValue = _GetRemoteFakeIPForConnection( Self, hConn, pOutAddr ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateFakeUDPPort", CallingConvention = Platform.CC)] + private static extern IntPtr _CreateFakeUDPPort( IntPtr self, int idxFakeServerPort ); + + #endregion + internal IntPtr CreateFakeUDPPort( int idxFakeServerPort ) + { + var returnValue = _CreateFakeUDPPort( Self, idxFakeServerPort ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs index 51739641e..50d6d7d34 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamNetworkingUtils : SteamInterface + internal unsafe class ISteamNetworkingUtils : SteamInterface { internal ISteamNetworkingUtils( bool IsGameServer ) @@ -15,20 +15,20 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingUtils_v003", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamNetworkingUtils_v003(); - public override IntPtr GetGlobalInterfacePointer() => SteamAPI_SteamNetworkingUtils_v003(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingUtils_SteamAPI_v004", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamNetworkingUtils_SteamAPI_v004(); + public override IntPtr GetGlobalInterfacePointer() => SteamAPI_SteamNetworkingUtils_SteamAPI_v004(); #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_AllocateMessage", CallingConvention = Platform.CC)] - private static extern IntPtr _AllocateMessage( IntPtr self, int cbAllocateBuffer ); + private static extern NetMsg* _AllocateMessage( IntPtr self, int cbAllocateBuffer ); #endregion - internal NetMsg AllocateMessage( int cbAllocateBuffer ) + internal NetMsg* AllocateMessage( int cbAllocateBuffer ) { var returnValue = _AllocateMessage( Self, cbAllocateBuffer ); - return returnValue.ToType(); + return returnValue; } #region FunctionMeta @@ -92,8 +92,7 @@ namespace Steamworks #endregion internal void ConvertPingLocationToString( ref NetPingLocation location, out string pszBuf ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszBuf = memory; + using var mempszBuf = Helpers.TakeMemory(); _ConvertPingLocationToString( Self, ref location, mempszBuf, (1024 * 32) ); pszBuf = Helpers.MemoryToString( mempszBuf ); } @@ -187,6 +186,40 @@ namespace Steamworks _SetDebugOutputFunction( Self, eDetailLevel, pfnFunc ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_IsFakeIPv4", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _IsFakeIPv4( IntPtr self, uint nIPv4 ); + + #endregion + internal bool IsFakeIPv4( uint nIPv4 ) + { + var returnValue = _IsFakeIPv4( Self, nIPv4 ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetIPv4FakeIPType", CallingConvention = Platform.CC)] + private static extern SteamNetworkingFakeIPType _GetIPv4FakeIPType( IntPtr self, uint nIPv4 ); + + #endregion + internal SteamNetworkingFakeIPType GetIPv4FakeIPType( uint nIPv4 ) + { + var returnValue = _GetIPv4FakeIPType( Self, nIPv4 ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetRealIdentityForFakeIP", CallingConvention = Platform.CC)] + private static extern Result _GetRealIdentityForFakeIP( IntPtr self, ref NetAddress fakeIP, [In,Out] NetIdentity[] pOutRealIdentity ); + + #endregion + internal Result GetRealIdentityForFakeIP( ref NetAddress fakeIP, [In,Out] NetIdentity[] pOutRealIdentity ) + { + var returnValue = _GetRealIdentityForFakeIP( Self, ref fakeIP, pOutRealIdentity ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalConfigValueInt32", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -223,6 +256,18 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalConfigValuePtr", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalConfigValuePtr( IntPtr self, NetConfig eValue, IntPtr val ); + + #endregion + internal bool SetGlobalConfigValuePtr( NetConfig eValue, IntPtr val ) + { + var returnValue = _SetGlobalConfigValuePtr( Self, eValue, val ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetConnectionConfigValueInt32", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -259,6 +304,78 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_SteamNetConnectionStatusChanged", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_SteamNetConnectionStatusChanged( IntPtr self, FnSteamNetConnectionStatusChanged fnCallback ); + + #endregion + internal bool SetGlobalCallback_SteamNetConnectionStatusChanged( FnSteamNetConnectionStatusChanged fnCallback ) + { + var returnValue = _SetGlobalCallback_SteamNetConnectionStatusChanged( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_SteamNetAuthenticationStatusChanged", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_SteamNetAuthenticationStatusChanged( IntPtr self, FnSteamNetAuthenticationStatusChanged fnCallback ); + + #endregion + internal bool SetGlobalCallback_SteamNetAuthenticationStatusChanged( FnSteamNetAuthenticationStatusChanged fnCallback ) + { + var returnValue = _SetGlobalCallback_SteamNetAuthenticationStatusChanged( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_SteamRelayNetworkStatusChanged", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_SteamRelayNetworkStatusChanged( IntPtr self, FnSteamRelayNetworkStatusChanged fnCallback ); + + #endregion + internal bool SetGlobalCallback_SteamRelayNetworkStatusChanged( FnSteamRelayNetworkStatusChanged fnCallback ) + { + var returnValue = _SetGlobalCallback_SteamRelayNetworkStatusChanged( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_FakeIPResult", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_FakeIPResult( IntPtr self, FnSteamNetworkingFakeIPResult fnCallback ); + + #endregion + internal bool SetGlobalCallback_FakeIPResult( FnSteamNetworkingFakeIPResult fnCallback ) + { + var returnValue = _SetGlobalCallback_FakeIPResult( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_MessagesSessionRequest", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_MessagesSessionRequest( IntPtr self, FnSteamNetworkingMessagesSessionRequest fnCallback ); + + #endregion + internal bool SetGlobalCallback_MessagesSessionRequest( FnSteamNetworkingMessagesSessionRequest fnCallback ) + { + var returnValue = _SetGlobalCallback_MessagesSessionRequest( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_MessagesSessionFailed", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_MessagesSessionFailed( IntPtr self, FnSteamNetworkingMessagesSessionFailed fnCallback ); + + #endregion + internal bool SetGlobalCallback_MessagesSessionFailed( FnSteamNetworkingMessagesSessionFailed fnCallback ) + { + var returnValue = _SetGlobalCallback_MessagesSessionFailed( Self, fnCallback ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetConfigValue", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -296,24 +413,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetConfigValueInfo", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetConfigValueInfo( IntPtr self, NetConfig eValue, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pOutName, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope, [In,Out] NetConfig[] pOutNextValue ); + private static extern Utf8StringPointer _GetConfigValueInfo( IntPtr self, NetConfig eValue, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope ); #endregion - internal bool GetConfigValueInfo( NetConfig eValue, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pOutName, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope, [In,Out] NetConfig[] pOutNextValue ) + internal string GetConfigValueInfo( NetConfig eValue, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope ) { - var returnValue = _GetConfigValueInfo( Self, eValue, pOutName, ref pOutDataType, pOutScope, pOutNextValue ); + var returnValue = _GetConfigValueInfo( Self, eValue, ref pOutDataType, pOutScope ); return returnValue; } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetFirstConfigValue", CallingConvention = Platform.CC)] - private static extern NetConfig _GetFirstConfigValue( IntPtr self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_IterateGenericEditableConfigValues", CallingConvention = Platform.CC)] + private static extern NetConfig _IterateGenericEditableConfigValues( IntPtr self, NetConfig eCurrent, [MarshalAs( UnmanagedType.U1 )] bool bEnumerateDevVars ); #endregion - internal NetConfig GetFirstConfigValue() + internal NetConfig IterateGenericEditableConfigValues( NetConfig eCurrent, [MarshalAs( UnmanagedType.U1 )] bool bEnumerateDevVars ) { - var returnValue = _GetFirstConfigValue( Self ); + var returnValue = _IterateGenericEditableConfigValues( Self, eCurrent, bEnumerateDevVars ); return returnValue; } @@ -324,8 +440,7 @@ namespace Steamworks #endregion internal void SteamNetworkingIPAddr_ToString( ref NetAddress addr, out string buf, [MarshalAs( UnmanagedType.U1 )] bool bWithPort ) { - using var memory = Helpers.TakeMemory(); - IntPtr membuf = memory; + using var membuf = Helpers.TakeMemory(); _SteamNetworkingIPAddr_ToString( Self, ref addr, membuf, (1024 * 32), bWithPort ); buf = Helpers.MemoryToString( membuf ); } @@ -342,6 +457,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SteamNetworkingIPAddr_GetFakeIPType", CallingConvention = Platform.CC)] + private static extern SteamNetworkingFakeIPType _SteamNetworkingIPAddr_GetFakeIPType( IntPtr self, ref NetAddress addr ); + + #endregion + internal SteamNetworkingFakeIPType SteamNetworkingIPAddr_GetFakeIPType( ref NetAddress addr ) + { + var returnValue = _SteamNetworkingIPAddr_GetFakeIPType( Self, ref addr ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SteamNetworkingIdentity_ToString", CallingConvention = Platform.CC)] private static extern void _SteamNetworkingIdentity_ToString( IntPtr self, ref NetIdentity identity, IntPtr buf, uint cbBuf ); @@ -349,8 +475,7 @@ namespace Steamworks #endregion internal void SteamNetworkingIdentity_ToString( ref NetIdentity identity, out string buf ) { - using var memory = Helpers.TakeMemory(); - IntPtr membuf = memory; + using var membuf = Helpers.TakeMemory(); _SteamNetworkingIdentity_ToString( Self, ref identity, membuf, (1024 * 32) ); buf = Helpers.MemoryToString( membuf ); } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs index 3968079e3..56bfb1e61 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamParentalSettings : SteamInterface + internal unsafe class ISteamParentalSettings : SteamInterface { internal ISteamParentalSettings( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs index 6adabea80..e9585ec8b 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamParties : SteamInterface + internal unsafe class ISteamParties : SteamInterface { internal ISteamParties( bool IsGameServer ) @@ -50,8 +50,7 @@ namespace Steamworks #endregion internal bool GetBeaconDetails( PartyBeaconID_t ulBeaconID, ref SteamId pSteamIDBeaconOwner, ref SteamPartyBeaconLocation_t pLocation, out string pchMetadata ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchMetadata = memory; + using var mempchMetadata = Helpers.TakeMemory(); var returnValue = _GetBeaconDetails( Self, ulBeaconID, ref pSteamIDBeaconOwner, ref pLocation, mempchMetadata, (1024 * 32) ); pchMetadata = Helpers.MemoryToString( mempchMetadata ); return returnValue; @@ -154,8 +153,7 @@ namespace Steamworks #endregion internal bool GetBeaconLocationData( SteamPartyBeaconLocation_t BeaconLocation, SteamPartyBeaconLocationData eData, out string pchDataStringOut ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchDataStringOut = memory; + using var mempchDataStringOut = Helpers.TakeMemory(); var returnValue = _GetBeaconLocationData( Self, BeaconLocation, eData, mempchDataStringOut, (1024 * 32) ); pchDataStringOut = Helpers.MemoryToString( mempchDataStringOut ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs index f2d98c20b..6ad42043c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamRemotePlay : SteamInterface + internal unsafe class ISteamRemotePlay : SteamInterface { internal ISteamRemotePlay( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs index cc2ee6b36..c1a872115 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamRemoteStorage : SteamInterface + internal unsafe class ISteamRemoteStorage : SteamInterface { internal ISteamRemoteStorage( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamRemoteStorage_v014", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamRemoteStorage_v014(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamRemoteStorage_v014(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamRemoteStorage_v016", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamRemoteStorage_v016(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamRemoteStorage_v016(); #region FunctionMeta @@ -375,5 +375,51 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_GetLocalFileChangeCount", CallingConvention = Platform.CC)] + private static extern int _GetLocalFileChangeCount( IntPtr self ); + + #endregion + internal int GetLocalFileChangeCount() + { + var returnValue = _GetLocalFileChangeCount( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_GetLocalFileChange", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetLocalFileChange( IntPtr self, int iFile, ref RemoteStorageLocalFileChange pEChangeType, ref RemoteStorageFilePathType pEFilePathType ); + + #endregion + internal string GetLocalFileChange( int iFile, ref RemoteStorageLocalFileChange pEChangeType, ref RemoteStorageFilePathType pEFilePathType ) + { + var returnValue = _GetLocalFileChange( Self, iFile, ref pEChangeType, ref pEFilePathType ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_BeginFileWriteBatch", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BeginFileWriteBatch( IntPtr self ); + + #endregion + internal bool BeginFileWriteBatch() + { + var returnValue = _BeginFileWriteBatch( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_EndFileWriteBatch", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _EndFileWriteBatch( IntPtr self ); + + #endregion + internal bool EndFileWriteBatch() + { + var returnValue = _EndFileWriteBatch( Self ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs index c076519a8..ff9380a3f 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamScreenshots : SteamInterface + internal unsafe class ISteamScreenshots : SteamInterface { internal ISteamScreenshots( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs deleted file mode 100644 index 1a8a6badb..000000000 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Steamworks.Data; - - -namespace Steamworks -{ - internal class ISteamTV : SteamInterface - { - - internal ISteamTV( bool IsGameServer ) - { - SetupInterface( IsGameServer ); - } - - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamTV_v001", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamTV_v001(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamTV_v001(); - - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_IsBroadcasting", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _IsBroadcasting( IntPtr self, ref int pnNumViewers ); - - #endregion - internal bool IsBroadcasting( ref int pnNumViewers ) - { - var returnValue = _IsBroadcasting( Self, ref pnNumViewers ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_AddBroadcastGameData", CallingConvention = Platform.CC)] - private static extern void _AddBroadcastGameData( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchValue ); - - #endregion - internal void AddBroadcastGameData( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchValue ) - { - _AddBroadcastGameData( Self, pchKey, pchValue ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_RemoveBroadcastGameData", CallingConvention = Platform.CC)] - private static extern void _RemoveBroadcastGameData( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey ); - - #endregion - internal void RemoveBroadcastGameData( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey ) - { - _RemoveBroadcastGameData( Self, pchKey ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_AddTimelineMarker", CallingConvention = Platform.CC)] - private static extern void _AddTimelineMarker( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTemplateName, [MarshalAs( UnmanagedType.U1 )] bool bPersistent, byte nColorR, byte nColorG, byte nColorB ); - - #endregion - internal void AddTimelineMarker( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTemplateName, [MarshalAs( UnmanagedType.U1 )] bool bPersistent, byte nColorR, byte nColorG, byte nColorB ) - { - _AddTimelineMarker( Self, pchTemplateName, bPersistent, nColorR, nColorG, nColorB ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_RemoveTimelineMarker", CallingConvention = Platform.CC)] - private static extern void _RemoveTimelineMarker( IntPtr self ); - - #endregion - internal void RemoveTimelineMarker() - { - _RemoveTimelineMarker( Self ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_AddRegion", CallingConvention = Platform.CC)] - private static extern uint _AddRegion( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchElementName, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTimelineDataSection, ref SteamTVRegion_t pSteamTVRegion, SteamTVRegionBehavior eSteamTVRegionBehavior ); - - #endregion - internal uint AddRegion( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchElementName, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTimelineDataSection, ref SteamTVRegion_t pSteamTVRegion, SteamTVRegionBehavior eSteamTVRegionBehavior ) - { - var returnValue = _AddRegion( Self, pchElementName, pchTimelineDataSection, ref pSteamTVRegion, eSteamTVRegionBehavior ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_RemoveRegion", CallingConvention = Platform.CC)] - private static extern void _RemoveRegion( IntPtr self, uint unRegionHandle ); - - #endregion - internal void RemoveRegion( uint unRegionHandle ) - { - _RemoveRegion( Self, unRegionHandle ); - } - - } -} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs index dbe560c05..758020c64 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUGC : SteamInterface + internal unsafe class ISteamUGC : SteamInterface { internal ISteamUGC( bool IsGameServer ) @@ -15,12 +15,12 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUGC_v014", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUGC_v014(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUGC_v014(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUGC_v014", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerUGC_v014(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUGC_v014(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUGC_v017", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUGC_v017(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUGC_v017(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUGC_v017", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerUGC_v017(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUGC_v017(); #region FunctionMeta @@ -90,6 +90,45 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCNumTags", CallingConvention = Platform.CC)] + private static extern uint _GetQueryUGCNumTags( IntPtr self, UGCQueryHandle_t handle, uint index ); + + #endregion + internal uint GetQueryUGCNumTags( UGCQueryHandle_t handle, uint index ) + { + var returnValue = _GetQueryUGCNumTags( Self, handle, index ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCTag", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetQueryUGCTag( IntPtr self, UGCQueryHandle_t handle, uint index, uint indexTag, IntPtr pchValue, uint cchValueSize ); + + #endregion + internal bool GetQueryUGCTag( UGCQueryHandle_t handle, uint index, uint indexTag, out string pchValue ) + { + using var mempchValue = Helpers.TakeMemory(); + var returnValue = _GetQueryUGCTag( Self, handle, index, indexTag, mempchValue, (1024 * 32) ); + pchValue = Helpers.MemoryToString( mempchValue ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCTagDisplayName", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetQueryUGCTagDisplayName( IntPtr self, UGCQueryHandle_t handle, uint index, uint indexTag, IntPtr pchValue, uint cchValueSize ); + + #endregion + internal bool GetQueryUGCTagDisplayName( UGCQueryHandle_t handle, uint index, uint indexTag, out string pchValue ) + { + using var mempchValue = Helpers.TakeMemory(); + var returnValue = _GetQueryUGCTagDisplayName( Self, handle, index, indexTag, mempchValue, (1024 * 32) ); + pchValue = Helpers.MemoryToString( mempchValue ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCPreviewURL", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -98,8 +137,7 @@ namespace Steamworks #endregion internal bool GetQueryUGCPreviewURL( UGCQueryHandle_t handle, uint index, out string pchURL ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchURL = memory; + using var mempchURL = Helpers.TakeMemory(); var returnValue = _GetQueryUGCPreviewURL( Self, handle, index, mempchURL, (1024 * 32) ); pchURL = Helpers.MemoryToString( mempchURL ); return returnValue; @@ -113,8 +151,7 @@ namespace Steamworks #endregion internal bool GetQueryUGCMetadata( UGCQueryHandle_t handle, uint index, out string pchMetadata ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchMetadata = memory; + using var mempchMetadata = Helpers.TakeMemory(); var returnValue = _GetQueryUGCMetadata( Self, handle, index, mempchMetadata, (1024 * 32) ); pchMetadata = Helpers.MemoryToString( mempchMetadata ); return returnValue; @@ -163,10 +200,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCAdditionalPreview( UGCQueryHandle_t handle, uint index, uint previewIndex, out string pchURLOrVideoID, out string pchOriginalFileName, ref ItemPreviewType pPreviewType ) { - using var memoryUrlOrId = Helpers.TakeMemory(); - using var memoryFileName = Helpers.TakeMemory(); - IntPtr mempchURLOrVideoID = memoryUrlOrId; - IntPtr mempchOriginalFileName = memoryFileName; + using var mempchURLOrVideoID = Helpers.TakeMemory(); + using var mempchOriginalFileName = Helpers.TakeMemory(); var returnValue = _GetQueryUGCAdditionalPreview( Self, handle, index, previewIndex, mempchURLOrVideoID, (1024 * 32), mempchOriginalFileName, (1024 * 32), ref pPreviewType ); pchURLOrVideoID = Helpers.MemoryToString( mempchURLOrVideoID ); pchOriginalFileName = Helpers.MemoryToString( mempchOriginalFileName ); @@ -192,10 +227,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCKeyValueTag( UGCQueryHandle_t handle, uint index, uint keyValueTagIndex, out string pchKey, out string pchValue ) { - using var memoryKey = Helpers.TakeMemory(); - using var memoryValue = Helpers.TakeMemory(); - IntPtr mempchKey = memoryKey; - IntPtr mempchValue = memoryValue; + using var mempchKey = Helpers.TakeMemory(); + using var mempchValue = Helpers.TakeMemory(); var returnValue = _GetQueryUGCKeyValueTag( Self, handle, index, keyValueTagIndex, mempchKey, (1024 * 32), mempchValue, (1024 * 32) ); pchKey = Helpers.MemoryToString( mempchKey ); pchValue = Helpers.MemoryToString( mempchValue ); @@ -210,13 +243,23 @@ namespace Steamworks #endregion internal bool GetQueryUGCKeyValueTag( UGCQueryHandle_t handle, uint index, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, out string pchValue ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchValue = memory; + using var mempchValue = Helpers.TakeMemory(); var returnValue = _GetQueryUGCKeyValueTag( Self, handle, index, pchKey, mempchValue, (1024 * 32) ); pchValue = Helpers.MemoryToString( mempchValue ); return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCContentDescriptors", CallingConvention = Platform.CC)] + private static extern uint _GetQueryUGCContentDescriptors( IntPtr self, UGCQueryHandle_t handle, uint index, [In,Out] UGCContentDescriptorID[] pvecDescriptors, uint cMaxEntries ); + + #endregion + internal uint GetQueryUGCContentDescriptors( UGCQueryHandle_t handle, uint index, [In,Out] UGCContentDescriptorID[] pvecDescriptors, uint cMaxEntries ) + { + var returnValue = _GetQueryUGCContentDescriptors( Self, handle, index, pvecDescriptors, cMaxEntries ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_ReleaseQueryUGCRequest", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -433,6 +476,30 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_SetTimeCreatedDateRange", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetTimeCreatedDateRange( IntPtr self, UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ); + + #endregion + internal bool SetTimeCreatedDateRange( UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ) + { + var returnValue = _SetTimeCreatedDateRange( Self, handle, rtStart, rtEnd ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_SetTimeUpdatedDateRange", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetTimeUpdatedDateRange( IntPtr self, UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ); + + #endregion + internal bool SetTimeUpdatedDateRange( UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ) + { + var returnValue = _SetTimeUpdatedDateRange( Self, handle, rtStart, rtEnd ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_AddRequiredKeyValueTag", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -671,6 +738,30 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_AddContentDescriptor", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _AddContentDescriptor( IntPtr self, UGCUpdateHandle_t handle, UGCContentDescriptorID descid ); + + #endregion + internal bool AddContentDescriptor( UGCUpdateHandle_t handle, UGCContentDescriptorID descid ) + { + var returnValue = _AddContentDescriptor( Self, handle, descid ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_RemoveContentDescriptor", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _RemoveContentDescriptor( IntPtr self, UGCUpdateHandle_t handle, UGCContentDescriptorID descid ); + + #endregion + internal bool RemoveContentDescriptor( UGCUpdateHandle_t handle, UGCContentDescriptorID descid ) + { + var returnValue = _RemoveContentDescriptor( Self, handle, descid ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_SubmitItemUpdate", CallingConvention = Platform.CC)] private static extern SteamAPICall_t _SubmitItemUpdate( IntPtr self, UGCUpdateHandle_t handle, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchChangeNote ); @@ -800,8 +891,7 @@ namespace Steamworks #endregion internal bool GetItemInstallInfo( PublishedFileId nPublishedFileID, ref ulong punSizeOnDisk, out string pchFolder, ref uint punTimeStamp ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchFolder = memory; + using var mempchFolder = Helpers.TakeMemory(); var returnValue = _GetItemInstallInfo( Self, nPublishedFileID, ref punSizeOnDisk, mempchFolder, (1024 * 32), ref punTimeStamp ); pchFolder = Helpers.MemoryToString( mempchFolder ); return returnValue; @@ -952,5 +1042,28 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_ShowWorkshopEULA", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _ShowWorkshopEULA( IntPtr self ); + + #endregion + internal bool ShowWorkshopEULA() + { + var returnValue = _ShowWorkshopEULA( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetWorkshopEULAStatus", CallingConvention = Platform.CC)] + private static extern SteamAPICall_t _GetWorkshopEULAStatus( IntPtr self ); + + #endregion + internal CallResult GetWorkshopEULAStatus() + { + var returnValue = _GetWorkshopEULAStatus( Self ); + return new CallResult( returnValue, IsServer ); + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs index 77624f5f8..18147edf6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUser : SteamInterface + internal unsafe class ISteamUser : SteamInterface { internal ISteamUser( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUser_v020", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUser_v020(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUser_v020(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUser_v023", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUser_v023(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUser_v023(); #region FunctionMeta @@ -55,24 +55,24 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_InitiateGameConnection", CallingConvention = Platform.CC)] - private static extern int _InitiateGameConnection( IntPtr self, IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_InitiateGameConnection_DEPRECATED", CallingConvention = Platform.CC)] + private static extern int _InitiateGameConnection_DEPRECATED( IntPtr self, IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ); #endregion - internal int InitiateGameConnection( IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ) + internal int InitiateGameConnection_DEPRECATED( IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ) { - var returnValue = _InitiateGameConnection( Self, pAuthBlob, cbMaxAuthBlob, steamIDGameServer, unIPServer, usPortServer, bSecure ); + var returnValue = _InitiateGameConnection_DEPRECATED( Self, pAuthBlob, cbMaxAuthBlob, steamIDGameServer, unIPServer, usPortServer, bSecure ); return returnValue; } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_TerminateGameConnection", CallingConvention = Platform.CC)] - private static extern void _TerminateGameConnection( IntPtr self, uint unIPServer, ushort usPortServer ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_TerminateGameConnection_DEPRECATED", CallingConvention = Platform.CC)] + private static extern void _TerminateGameConnection_DEPRECATED( IntPtr self, uint unIPServer, ushort usPortServer ); #endregion - internal void TerminateGameConnection( uint unIPServer, ushort usPortServer ) + internal void TerminateGameConnection_DEPRECATED( uint unIPServer, ushort usPortServer ) { - _TerminateGameConnection( Self, unIPServer, usPortServer ); + _TerminateGameConnection_DEPRECATED( Self, unIPServer, usPortServer ); } #region FunctionMeta @@ -93,8 +93,7 @@ namespace Steamworks #endregion internal bool GetUserDataFolder( out string pchBuffer ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchBuffer = memory; + using var mempchBuffer = Helpers.TakeMemory(); var returnValue = _GetUserDataFolder( Self, mempchBuffer, (1024 * 32) ); pchBuffer = Helpers.MemoryToString( mempchBuffer ); return returnValue; @@ -166,12 +165,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_GetAuthSessionTicket", CallingConvention = Platform.CC)] - private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ); + private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSteamNetworkingIdentity ); #endregion - internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ) + internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSteamNetworkingIdentity ) { - var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket ); + var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket, ref pSteamNetworkingIdentity ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_GetAuthTicketForWebApi", CallingConvention = Platform.CC)] + private static extern HAuthTicket _GetAuthTicketForWebApi( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchIdentity ); + + #endregion + internal HAuthTicket GetAuthTicketForWebApi( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchIdentity ) + { + var returnValue = _GetAuthTicketForWebApi( Self, pchIdentity ); return returnValue; } @@ -365,5 +375,17 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_BSetDurationControlOnlineState", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BSetDurationControlOnlineState( IntPtr self, DurationControlOnlineState eNewState ); + + #endregion + internal bool BSetDurationControlOnlineState( DurationControlOnlineState eNewState ) + { + var returnValue = _BSetDurationControlOnlineState( Self, eNewState ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs index 69d46180b..ab98ca638 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUserStats : SteamInterface + internal unsafe class ISteamUserStats : SteamInterface { internal ISteamUserStats( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUserStats_v011", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUserStats_v011(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUserStats_v011(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUserStats_v012", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUserStats_v012(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUserStats_v012(); #region FunctionMeta @@ -361,9 +361,6 @@ namespace Steamworks private static extern SteamAPICall_t _DownloadLeaderboardEntriesForUsers( IntPtr self, SteamLeaderboard_t hSteamLeaderboard, [In,Out] SteamId[] prgUsers, int cUsers ); #endregion - /// - /// Downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers - /// internal CallResult DownloadLeaderboardEntriesForUsers( SteamLeaderboard_t hSteamLeaderboard, [In,Out] SteamId[] prgUsers, int cUsers ) { var returnValue = _DownloadLeaderboardEntriesForUsers( Self, hSteamLeaderboard, prgUsers, cUsers ); @@ -433,8 +430,7 @@ namespace Steamworks #endregion internal int GetMostAchievedAchievementInfo( out string pchName, ref float pflPercent, [MarshalAs( UnmanagedType.U1 )] ref bool pbAchieved ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetMostAchievedAchievementInfo( Self, mempchName, (1024 * 32), ref pflPercent, ref pbAchieved ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -447,8 +443,7 @@ namespace Steamworks #endregion internal int GetNextMostAchievedAchievementInfo( int iIteratorPrevious, out string pchName, ref float pflPercent, [MarshalAs( UnmanagedType.U1 )] ref bool pbAchieved ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetNextMostAchievedAchievementInfo( Self, iIteratorPrevious, mempchName, (1024 * 32), ref pflPercent, ref pbAchieved ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -523,5 +518,29 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUserStats_GetAchievementProgressLimitsInt32", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetAchievementProgressLimits( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref int pnMinProgress, ref int pnMaxProgress ); + + #endregion + internal bool GetAchievementProgressLimits( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref int pnMinProgress, ref int pnMaxProgress ) + { + var returnValue = _GetAchievementProgressLimits( Self, pchName, ref pnMinProgress, ref pnMaxProgress ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUserStats_GetAchievementProgressLimitsFloat", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetAchievementProgressLimits( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref float pfMinProgress, ref float pfMaxProgress ); + + #endregion + internal bool GetAchievementProgressLimits( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref float pfMinProgress, ref float pfMaxProgress ) + { + var returnValue = _GetAchievementProgressLimits( Self, pchName, ref pfMinProgress, ref pfMaxProgress ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs index aae959ae4..f0a4cf544 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUtils : SteamInterface + internal unsafe class ISteamUtils : SteamInterface { internal ISteamUtils( bool IsGameServer ) @@ -15,12 +15,12 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUtils_v009", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUtils_v009(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUtils_v009(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUtils_v009", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerUtils_v009(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUtils_v009(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUtils_v010", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUtils_v010(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUtils_v010(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUtils_v010", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerUtils_v010(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUtils_v010(); #region FunctionMeta @@ -102,18 +102,6 @@ namespace Steamworks return returnValue; } - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_GetCSERIPPort", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetCSERIPPort( IntPtr self, ref uint unIP, ref ushort usPort ); - - #endregion - internal bool GetCSERIPPort( ref uint unIP, ref ushort usPort ) - { - var returnValue = _GetCSERIPPort( Self, ref unIP, ref usPort ); - return returnValue; - } - #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_GetCurrentBatteryPower", CallingConvention = Platform.CC)] private static extern byte _GetCurrentBatteryPower( IntPtr self ); @@ -268,8 +256,7 @@ namespace Steamworks #endregion internal bool GetEnteredGamepadTextInput( out string pchText ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchText = memory; + using var mempchText = Helpers.TakeMemory(); var returnValue = _GetEnteredGamepadTextInput( Self, mempchText, (1024 * 32) ); pchText = Helpers.MemoryToString( mempchText ); return returnValue; @@ -367,25 +354,24 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_InitFilterText", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _InitFilterText( IntPtr self ); + private static extern bool _InitFilterText( IntPtr self, uint unFilterOptions ); #endregion - internal bool InitFilterText() + internal bool InitFilterText( uint unFilterOptions ) { - var returnValue = _InitFilterText( Self ); + var returnValue = _InitFilterText( Self, unFilterOptions ); return returnValue; } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_FilterText", CallingConvention = Platform.CC)] - private static extern int _FilterText( IntPtr self, IntPtr pchOutFilteredText, uint nByteSizeOutFilteredText, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, [MarshalAs( UnmanagedType.U1 )] bool bLegalOnly ); + private static extern int _FilterText( IntPtr self, TextFilteringContext eContext, SteamId sourceSteamID, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, IntPtr pchOutFilteredText, uint nByteSizeOutFilteredText ); #endregion - internal int FilterText( out string pchOutFilteredText, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, [MarshalAs( UnmanagedType.U1 )] bool bLegalOnly ) + internal int FilterText( TextFilteringContext eContext, SteamId sourceSteamID, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, out string pchOutFilteredText ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchOutFilteredText = memory; - var returnValue = _FilterText( Self, mempchOutFilteredText, (1024 * 32), pchInputMessage, bLegalOnly ); + using var mempchOutFilteredText = Helpers.TakeMemory(); + var returnValue = _FilterText( Self, eContext, sourceSteamID, pchInputMessage, mempchOutFilteredText, (1024 * 32) ); pchOutFilteredText = Helpers.MemoryToString( mempchOutFilteredText ); return returnValue; } @@ -401,5 +387,51 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_IsSteamRunningOnSteamDeck", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _IsSteamRunningOnSteamDeck( IntPtr self ); + + #endregion + internal bool IsSteamRunningOnSteamDeck() + { + var returnValue = _IsSteamRunningOnSteamDeck( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_ShowFloatingGamepadTextInput", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _ShowFloatingGamepadTextInput( IntPtr self, TextInputMode eKeyboardMode, int nTextFieldXPosition, int nTextFieldYPosition, int nTextFieldWidth, int nTextFieldHeight ); + + #endregion + internal bool ShowFloatingGamepadTextInput( TextInputMode eKeyboardMode, int nTextFieldXPosition, int nTextFieldYPosition, int nTextFieldWidth, int nTextFieldHeight ) + { + var returnValue = _ShowFloatingGamepadTextInput( Self, eKeyboardMode, nTextFieldXPosition, nTextFieldYPosition, nTextFieldWidth, nTextFieldHeight ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_SetGameLauncherMode", CallingConvention = Platform.CC)] + private static extern void _SetGameLauncherMode( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bLauncherMode ); + + #endregion + internal void SetGameLauncherMode( [MarshalAs( UnmanagedType.U1 )] bool bLauncherMode ) + { + _SetGameLauncherMode( Self, bLauncherMode ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_DismissFloatingGamepadTextInput", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _DismissFloatingGamepadTextInput( IntPtr self ); + + #endregion + internal bool DismissFloatingGamepadTextInput() + { + var returnValue = _DismissFloatingGamepadTextInput( Self ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs index c6c55105e..bd4d029a8 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamVideo : SteamInterface + internal unsafe class ISteamVideo : SteamInterface { internal ISteamVideo( bool IsGameServer ) @@ -60,8 +60,7 @@ namespace Steamworks #endregion internal bool GetOPFStringForApp( AppId unVideoAppID, out string pchBuffer, ref int pnBufferSize ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchBuffer = memory; + using var mempchBuffer = Helpers.TakeMemory(); var returnValue = _GetOPFStringForApp( Self, unVideoAppID, mempchBuffer, ref pnBufferSize ); pchBuffer = Helpers.MemoryToString( mempchBuffer ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs b/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs index fbc2d590a..883114778 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs @@ -206,6 +206,22 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct GetTicketForWebApiResponse_t : ICallbackData + { + internal uint AuthTicket; // m_hAuthTicket HAuthTicket + internal Result Result; // m_eResult EResult + internal int Ticket; // m_cubTicket int + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2560)] // m_rgubTicket + internal byte[] GubTicket; // m_rgubTicket uint8 [2560] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(GetTicketForWebApiResponse_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.GetTicketForWebApiResponse; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct PersonaStateChange_t : ICallbackData { @@ -223,6 +239,9 @@ namespace Steamworks.Data internal struct GameOverlayActivated_t : ICallbackData { internal byte Active; // m_bActive uint8 + [MarshalAs(UnmanagedType.I1)] + internal bool UserInitiated; // m_bUserInitiated bool + internal AppId AppID; // m_nAppID AppId_t #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(GameOverlayActivated_t) ); @@ -473,6 +492,55 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct OverlayBrowserProtocolNavigation_t : ICallbackData + { + internal string RgchURIUTF8() => System.Text.Encoding.UTF8.GetString( RgchURI, 0, System.Array.IndexOf( RgchURI, 0 ) ); + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)] // byte[] rgchURI + internal byte[] RgchURI; // rgchURI char [1024] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(OverlayBrowserProtocolNavigation_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.OverlayBrowserProtocolNavigation; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct EquippedProfileItemsChanged_t : ICallbackData + { + internal ulong SteamID; // m_steamID CSteamID + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(EquippedProfileItemsChanged_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.EquippedProfileItemsChanged; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )] + internal struct EquippedProfileItems_t : ICallbackData + { + internal Result Result; // m_eResult EResult + internal ulong SteamID; // m_steamID CSteamID + [MarshalAs(UnmanagedType.I1)] + internal bool HasAnimatedAvatar; // m_bHasAnimatedAvatar bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasAvatarFrame; // m_bHasAvatarFrame bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasProfileModifier; // m_bHasProfileModifier bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasProfileBackground; // m_bHasProfileBackground bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasMiniProfileBackground; // m_bHasMiniProfileBackground bool + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(EquippedProfileItems_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.EquippedProfileItems; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct IPCountry_t : ICallbackData { @@ -539,6 +607,7 @@ namespace Steamworks.Data [MarshalAs(UnmanagedType.I1)] internal bool Submitted; // m_bSubmitted bool internal uint SubmittedText; // m_unSubmittedText uint32 + internal AppId AppID; // m_unAppID AppId_t #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(GamepadTextInputDismissed_t) ); @@ -547,6 +616,40 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct AppResumingFromSuspend_t : ICallbackData + { + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(AppResumingFromSuspend_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.AppResumingFromSuspend; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct FloatingGamepadTextInputDismissed_t : ICallbackData + { + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(FloatingGamepadTextInputDismissed_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.FloatingGamepadTextInputDismissed; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct FilterTextDictionaryChanged_t : ICallbackData + { + internal int Language; // m_eLanguage int + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(FilterTextDictionaryChanged_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.FilterTextDictionaryChanged; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct FavoritesListChanged_t : ICallbackData { @@ -914,66 +1017,6 @@ namespace Steamworks.Data #endregion } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncedClient_t : ICallbackData - { - internal AppId AppID; // m_nAppID AppId_t - internal Result Result; // m_eResult EResult - internal int NumDownloads; // m_unNumDownloads int - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncedClient_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncedClient; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncedServer_t : ICallbackData - { - internal AppId AppID; // m_nAppID AppId_t - internal Result Result; // m_eResult EResult - internal int NumUploads; // m_unNumUploads int - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncedServer_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncedServer; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncProgress_t : ICallbackData - { - internal string CurrentFileUTF8() => System.Text.Encoding.UTF8.GetString( CurrentFile, 0, System.Array.IndexOf( CurrentFile, 0 ) ); - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)] // byte[] m_rgchCurrentFile - internal byte[] CurrentFile; // m_rgchCurrentFile char [260] - internal AppId AppID; // m_nAppID AppId_t - internal uint BytesTransferredThisChunk; // m_uBytesTransferredThisChunk uint32 - internal double DAppPercentComplete; // m_dAppPercentComplete double - [MarshalAs(UnmanagedType.I1)] - internal bool Uploading; // m_bUploading bool - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncProgress_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncProgress; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncStatusCheck_t : ICallbackData - { - internal AppId AppID; // m_nAppID AppId_t - internal Result Result; // m_eResult EResult - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncStatusCheck_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncStatusCheck; - #endregion - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct RemoteStorageFileShareResult_t : ICallbackData { @@ -1364,6 +1407,17 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct RemoteStorageLocalFileChange_t : ICallbackData + { + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageLocalFileChange_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.RemoteStorageLocalFileChange; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )] internal struct UserStatsReceived_t : ICallbackData { @@ -1548,19 +1602,6 @@ namespace Steamworks.Data #endregion } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RegisterActivationCodeResponse_t : ICallbackData - { - internal RegisterActivationCodeResult Result; // m_eResult ERegisterActivationCodeResult - internal uint PackageRegistered; // m_unPackageRegistered uint32 - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RegisterActivationCodeResponse_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RegisterActivationCodeResponse; - #endregion - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct NewUrlLaunchParameters_t : ICallbackData { @@ -1605,6 +1646,22 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct TimedTrialStatus_t : ICallbackData + { + internal AppId AppID; // m_unAppID AppId_t + [MarshalAs(UnmanagedType.I1)] + internal bool IsOffline; // m_bIsOffline bool + internal uint SecondsAllowed; // m_unSecondsAllowed uint32 + internal uint SecondsPlayed; // m_unSecondsPlayed uint32 + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(TimedTrialStatus_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.TimedTrialStatus; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct P2PSessionRequest_t : ICallbackData { @@ -1884,6 +1941,66 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputDeviceConnected_t : ICallbackData + { + internal ulong ConnectedDeviceHandle; // m_ulConnectedDeviceHandle InputHandle_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputDeviceConnected_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputDeviceConnected; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputDeviceDisconnected_t : ICallbackData + { + internal ulong DisconnectedDeviceHandle; // m_ulDisconnectedDeviceHandle InputHandle_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputDeviceDisconnected_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputDeviceDisconnected; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )] + internal struct SteamInputConfigurationLoaded_t : ICallbackData + { + internal AppId AppID; // m_unAppID AppId_t + internal ulong DeviceHandle; // m_ulDeviceHandle InputHandle_t + internal ulong MappingCreator; // m_ulMappingCreator CSteamID + internal uint MajorRevision; // m_unMajorRevision uint32 + internal uint MinorRevision; // m_unMinorRevision uint32 + [MarshalAs(UnmanagedType.I1)] + internal bool UsesSteamInputAPI; // m_bUsesSteamInputAPI bool + [MarshalAs(UnmanagedType.I1)] + internal bool UsesGamepadAPI; // m_bUsesGamepadAPI bool + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputConfigurationLoaded_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputConfigurationLoaded; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputGamepadSlotChange_t : ICallbackData + { + internal AppId AppID; // m_unAppID AppId_t + internal ulong DeviceHandle; // m_ulDeviceHandle InputHandle_t + internal InputType DeviceType; // m_eDeviceType ESteamInputType + internal int OldGamepadSlot; // m_nOldGamepadSlot int + internal int NewGamepadSlot; // m_nNewGamepadSlot int + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputGamepadSlotChange_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputGamepadSlotChange; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamUGCQueryCompleted_t : ICallbackData { @@ -2134,10 +2251,42 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct UserSubscribedItemsListChanged_t : ICallbackData + { + internal AppId AppID; // m_nAppID AppId_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(UserSubscribedItemsListChanged_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.UserSubscribedItemsListChanged; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct WorkshopEULAStatus_t : ICallbackData + { + internal Result Result; // m_eResult EResult + internal AppId AppID; // m_nAppID AppId_t + internal uint Version; // m_unVersion uint32 + internal uint TAction; // m_rtAction RTime32 + [MarshalAs(UnmanagedType.I1)] + internal bool Accepted; // m_bAccepted bool + [MarshalAs(UnmanagedType.I1)] + internal bool NeedsAction; // m_bNeedsAction bool + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(WorkshopEULAStatus_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.WorkshopEULAStatus; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamAppInstalled_t : ICallbackData { internal AppId AppID; // m_nAppID AppId_t + internal int InstallFolderIndex; // m_iInstallFolderIndex int #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamAppInstalled_t) ); @@ -2150,6 +2299,7 @@ namespace Steamworks.Data internal struct SteamAppUninstalled_t : ICallbackData { internal AppId AppID; // m_nAppID AppId_t + internal int InstallFolderIndex; // m_iInstallFolderIndex int #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamAppUninstalled_t) ); @@ -2611,31 +2761,6 @@ namespace Steamworks.Data #endregion } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct BroadcastUploadStart_t : ICallbackData - { - [MarshalAs(UnmanagedType.I1)] - internal bool IsRTMP; // m_bIsRTMP bool - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(BroadcastUploadStart_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.BroadcastUploadStart; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct BroadcastUploadStop_t : ICallbackData - { - internal BroadcastUploadResult Result; // m_eResult EBroadcastUploadResult - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(BroadcastUploadStop_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.BroadcastUploadStop; - #endregion - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamParentalSettingsChanged_t : ICallbackData { @@ -2671,6 +2796,44 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamRemotePlayTogetherGuestInvite_t : ICallbackData + { + internal string ConnectURLUTF8() => System.Text.Encoding.UTF8.GetString( ConnectURL, 0, System.Array.IndexOf( ConnectURL, 0 ) ); + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)] // byte[] m_szConnectURL + internal byte[] ConnectURL; // m_szConnectURL char [1024] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamRemotePlayTogetherGuestInvite_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamRemotePlayTogetherGuestInvite; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamNetworkingMessagesSessionRequest_t : ICallbackData + { + internal NetIdentity DentityRemote; // m_identityRemote SteamNetworkingIdentity + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamNetworkingMessagesSessionRequest_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamNetworkingMessagesSessionRequest; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamNetworkingMessagesSessionFailed_t : ICallbackData + { + internal ConnectionInfo Nfo; // m_info SteamNetConnectionInfo_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamNetworkingMessagesSessionFailed_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamNetworkingMessagesSessionFailed; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamNetConnectionStatusChangedCallback_t : ICallbackData { @@ -2906,4 +3069,20 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamNetworkingFakeIPResult_t : ICallbackData + { + internal Result Result; // m_eResult EResult + internal NetIdentity Dentity; // m_identity SteamNetworkingIdentity + internal uint IP; // m_unIP uint32 + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8, ArraySubType = UnmanagedType.U2)] + internal ushort[] Ports; // m_unPorts uint16 [8] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamNetworkingFakeIPResult_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamNetworkingFakeIPResult; + #endregion + } + } diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs b/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs index 521b00d28..fcdd12f1c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs @@ -8,22 +8,9 @@ namespace Steamworks.Data { internal static class Defines { - internal static readonly int k_cubSaltSize = 8; - internal static readonly GID_t k_GIDNil = 0xffffffffffffffff; - internal static readonly GID_t k_TxnIDNil = k_GIDNil; - internal static readonly GID_t k_TxnIDUnknown = 0; - internal static readonly JobID_t k_JobIDNil = 0xffffffffffffffff; - internal static readonly PackageId_t k_uPackageIdInvalid = 0xFFFFFFFF; - internal static readonly BundleId_t k_uBundleIdInvalid = 0; internal static readonly AppId k_uAppIdInvalid = 0x0; - internal static readonly AssetClassId_t k_ulAssetClassIdInvalid = 0x0; - internal static readonly PhysicalItemId_t k_uPhysicalItemIdInvalid = 0x0; internal static readonly DepotId_t k_uDepotIdInvalid = 0x0; - internal static readonly CellID_t k_uCellIDInvalid = 0xFFFFFFFF; internal static readonly SteamAPICall_t k_uAPICallInvalid = 0x0; - internal static readonly PartnerId_t k_uPartnerIdInvalid = 0; - internal static readonly ManifestId_t k_uManifestIdInvalid = 0; - internal static readonly SiteId_t k_ulSiteIdInvalid = 0; internal static readonly PartyBeaconID_t k_ulPartyBeaconIdInvalid = 0; internal static readonly HAuthTicket k_HAuthTicketInvalid = 0; internal static readonly uint k_unSteamAccountIDMask = 0xFFFFFFFF; @@ -77,6 +64,13 @@ namespace Steamworks.Data internal static readonly int k_cchMaxSteamNetworkingErrMsg = 1024; internal static readonly int k_cchSteamNetworkingMaxConnectionCloseReason = 128; internal static readonly int k_cchSteamNetworkingMaxConnectionDescription = 128; + internal static readonly int k_cchSteamNetworkingMaxConnectionAppName = 32; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Unauthenticated = 1; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Unencrypted = 2; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_LoopbackBuffers = 4; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Fast = 8; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Relayed = 16; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_DualWifi = 32; internal static readonly int k_cbMaxSteamNetworkingSocketsMessageSizeSend = 512 * 1024; internal static readonly int k_nSteamNetworkingSend_Unreliable = 0; internal static readonly int k_nSteamNetworkingSend_NoNagle = 1; @@ -86,19 +80,23 @@ namespace Steamworks.Data internal static readonly int k_nSteamNetworkingSend_Reliable = 8; internal static readonly int k_nSteamNetworkingSend_ReliableNoNagle = k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_NoNagle; internal static readonly int k_nSteamNetworkingSend_UseCurrentThread = 16; + internal static readonly int k_nSteamNetworkingSend_AutoRestartBrokenSession = 32; internal static readonly int k_cchMaxSteamNetworkingPingLocationString = 1024; internal static readonly int k_nSteamNetworkingPing_Failed = - 1; internal static readonly int k_nSteamNetworkingPing_Unknown = - 2; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Default = - 1; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Disable = 0; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Relay = 1; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Private = 2; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Public = 4; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_All = 0x7fffffff; internal static readonly SteamNetworkingPOPID k_SteamDatagramPOPID_dev = ( ( uint ) 'd' << 16 ) | ( ( uint ) 'e' << 8 ) | ( uint ) 'v'; - internal static readonly uint k_unServerFlagNone = 0x00; - internal static readonly uint k_unServerFlagActive = 0x01; - internal static readonly uint k_unServerFlagSecure = 0x02; - internal static readonly uint k_unServerFlagDedicated = 0x04; - internal static readonly uint k_unServerFlagLinux = 0x08; - internal static readonly uint k_unServerFlagPassworded = 0x10; - internal static readonly uint k_unServerFlagPrivate = 0x20; + internal static readonly ushort STEAMGAMESERVER_QUERY_PORT_SHARED = 0xffff; + internal static readonly ushort MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE = STEAMGAMESERVER_QUERY_PORT_SHARED; internal static readonly uint k_cbSteamDatagramMaxSerializedTicket = 512; internal static readonly uint k_cbMaxSteamDatagramGameCoordinatorServerLoginAppData = 2048; internal static readonly uint k_cbMaxSteamDatagramGameCoordinatorServerLoginSerialized = 4096; + internal static readonly int k_cbSteamNetworkingSocketsFakeUDPPortRecommendedMTU = 1200; + internal static readonly int k_cbSteamNetworkingSocketsFakeUDPPortMaxMessageSize = 4096; } } diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs b/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs index 253ef88d2..0dc3329ac 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs @@ -148,6 +148,18 @@ namespace Steamworks CantRemoveItem = 113, AccountDeleted = 114, ExistingUserCancelledLicense = 115, + CommunityCooldown = 116, + NoLauncherSpecified = 117, + MustAgreeToSSA = 118, + LauncherMigrated = 119, + SteamRealmMismatch = 120, + InvalidSignature = 121, + ParseFailure = 122, + NoVerifiedPhone = 123, + InsufficientBattery = 124, + ChargerRequired = 125, + CachedCredentialInvalid = 126, + K_EResultPhoneNumberIsVOIP = 127, } // @@ -222,6 +234,7 @@ namespace Steamworks AuthTicketInvalidAlreadyUsed = 7, AuthTicketInvalid = 8, PublisherIssuedBan = 9, + AuthTicketNetworkIdentityFailure = 10, } // @@ -253,88 +266,6 @@ namespace Steamworks Max = 11, } - // - // EAppReleaseState - // - internal enum AppReleaseState : int - { - Unknown = 0, - Unavailable = 1, - Prerelease = 2, - PreloadOnly = 3, - Released = 4, - } - - // - // EAppOwnershipFlags - // - internal enum AppOwnershipFlags : int - { - None = 0, - OwnsLicense = 1, - FreeLicense = 2, - RegionRestricted = 4, - LowViolence = 8, - InvalidPlatform = 16, - SharedLicense = 32, - FreeWeekend = 64, - RetailLicense = 128, - LicenseLocked = 256, - LicensePending = 512, - LicenseExpired = 1024, - LicensePermanent = 2048, - LicenseRecurring = 4096, - LicenseCanceled = 8192, - AutoGrant = 16384, - PendingGift = 32768, - RentalNotActivated = 65536, - Rental = 131072, - SiteLicense = 262144, - LegacyFreeSub = 524288, - InvalidOSType = 1048576, - } - - // - // EAppType - // - internal enum AppType : int - { - Invalid = 0, - Game = 1, - Application = 2, - Tool = 4, - Demo = 8, - Media_DEPRECATED = 16, - DLC = 32, - Guide = 64, - Driver = 128, - Config = 256, - Hardware = 512, - Franchise = 1024, - Video = 2048, - Plugin = 4096, - MusicAlbum = 8192, - Series = 16384, - Comic_UNUSED = 32768, - Beta = 65536, - Shortcut = 1073741824, - DepotOnly = -2147483648, - } - - // - // ESteamUserStatType - // - internal enum SteamUserStatType : int - { - INVALID = 0, - INT = 1, - FLOAT = 2, - AVGRATE = 3, - ACHIEVEMENTS = 4, - GROUPACHIEVEMENTS = 5, - MAX = 6, - } - // // EChatEntryType // @@ -384,24 +315,12 @@ namespace Steamworks InstanceFlagMMSLobby = 131072, } - // - // EMarketingMessageFlags - // - internal enum MarketingMessageFlags : int - { - None = 0, - HighPriority = 1, - PlatformWindows = 2, - PlatformMac = 4, - PlatformLinux = 8, - PlatformRestrictions = 14, - } - // // ENotificationPosition // public enum NotificationPosition : int { + Invalid = -1, TopLeft = 0, TopRight = 1, BottomLeft = 2, @@ -439,70 +358,6 @@ namespace Steamworks AudioInitFailed = 23, } - // - // ELaunchOptionType - // - internal enum LaunchOptionType : int - { - None = 0, - Default = 1, - SafeMode = 2, - Multiplayer = 3, - Config = 4, - OpenVR = 5, - Server = 6, - Editor = 7, - Manual = 8, - Benchmark = 9, - Option1 = 10, - Option2 = 11, - Option3 = 12, - OculusVR = 13, - OpenVROverlay = 14, - OSVR = 15, - Dialog = 1000, - } - - // - // EVRHMDType - // - internal enum VRHMDType : int - { - MDType_None = -1, - MDType_Unknown = 0, - MDType_HTC_Dev = 1, - MDType_HTC_VivePre = 2, - MDType_HTC_Vive = 3, - MDType_HTC_VivePro = 4, - MDType_HTC_ViveCosmos = 5, - MDType_HTC_Unknown = 20, - MDType_Oculus_DK1 = 21, - MDType_Oculus_DK2 = 22, - MDType_Oculus_Rift = 23, - MDType_Oculus_RiftS = 24, - MDType_Oculus_Quest = 25, - MDType_Oculus_Unknown = 40, - MDType_Acer_Unknown = 50, - MDType_Acer_WindowsMR = 51, - MDType_Dell_Unknown = 60, - MDType_Dell_Visor = 61, - MDType_Lenovo_Unknown = 70, - MDType_Lenovo_Explorer = 71, - MDType_HP_Unknown = 80, - MDType_HP_WindowsMR = 81, - MDType_HP_Reverb = 82, - MDType_Samsung_Unknown = 90, - MDType_Samsung_Odyssey = 91, - MDType_Unannounced_Unknown = 100, - MDType_Unannounced_WindowsMR = 101, - MDType_vridge = 110, - MDType_Huawei_Unknown = 120, - MDType_Huawei_VR2 = 121, - MDType_Huawei_EndOfRange = 129, - mdType_Valve_Unknown = 130, - mdType_Valve_Index = 131, - } - // // EMarketNotAllowedReasonFlags // @@ -555,6 +410,17 @@ namespace Steamworks ExitSoon_Night = 7, } + // + // EDurationControlOnlineState + // + internal enum DurationControlOnlineState : int + { + Invalid = 0, + Offline = 1, + Online = 2, + OnlineHighPri = 3, + } + // // EGameSearchErrorCode_t // @@ -672,7 +538,7 @@ namespace Steamworks // // EOverlayToStoreFlag // - internal enum OverlayToStoreFlag : int + public enum OverlayToStoreFlag : int { None = 0, AddToCart = 1, @@ -688,6 +554,37 @@ namespace Steamworks Modal = 1, } + // + // ECommunityProfileItemType + // + internal enum CommunityProfileItemType : int + { + AnimatedAvatar = 0, + AvatarFrame = 1, + ProfileModifier = 2, + ProfileBackground = 3, + MiniProfileBackground = 4, + } + + // + // ECommunityProfileItemProperty + // + internal enum CommunityProfileItemProperty : int + { + ImageSmall = 0, + ImageLarge = 1, + InternalName = 2, + Title = 3, + Description = 4, + AppID = 5, + TypeID = 6, + Class = 7, + MovieWebM = 8, + MovieMP4 = 9, + MovieWebMSmall = 10, + MovieMP4Small = 11, + } + // // EPersonaChange // @@ -740,6 +637,28 @@ namespace Steamworks MultipleLines = 1, } + // + // EFloatingGamepadTextInputMode + // + public enum TextInputMode : int + { + SingleLine = 0, + MultipleLines = 1, + Email = 2, + Numeric = 3, + } + + // + // ETextFilteringContext + // + public enum TextFilteringContext : int + { + Unknown = 0, + GameContent = 1, + Chat = 2, + Name = 3, + } + // // ECheckFileSignature // @@ -937,6 +856,26 @@ namespace Steamworks lose = 2, } + // + // ERemoteStorageLocalFileChange + // + internal enum RemoteStorageLocalFileChange : int + { + Invalid = 0, + FileUpdated = 1, + FileDeleted = 2, + } + + // + // ERemoteStorageFilePathType + // + internal enum RemoteStorageFilePathType : int + { + Invalid = 0, + Absolute = 1, + APIFilename = 2, + } + // // ELeaderboardDataRequest // @@ -964,28 +903,16 @@ namespace Steamworks ForceUpdate = 2, } - // - // ERegisterActivationCodeResult - // - internal enum RegisterActivationCodeResult : int - { - ResultOK = 0, - ResultFail = 1, - ResultAlreadyRegistered = 2, - ResultTimeout = 3, - AlreadyOwned = 4, - } - // // EP2PSessionError // public enum P2PSessionError : int { None = 0, - NotRunningApp = 1, NoRightsToApp = 2, - DestinationNotLoggedIn = 3, Timeout = 4, + NotRunningApp_DELETED = 1, + DestinationNotLoggedIn_DELETED = 3, Max = 5, } @@ -1067,6 +994,7 @@ namespace Steamworks Code304NotModified = 304, Code305UseProxy = 305, Code307TemporaryRedirect = 307, + Code308PermanentRedirect = 308, Code400BadRequest = 400, Code401Unauthorized = 401, Code402PaymentRequired = 402, @@ -1087,6 +1015,7 @@ namespace Steamworks Code417ExpectationFailed = 417, Code4xxUnknown = 418, Code429TooManyRequests = 429, + Code444ConnectionClosed = 444, Code500InternalServerError = 500, Code501NotImplemented = 501, Code502BadGateway = 502, @@ -1268,11 +1197,11 @@ namespace Steamworks XBoxOne_DPad_West = 140, XBoxOne_DPad_East = 141, XBoxOne_DPad_Move = 142, - XBoxOne_Reserved1 = 143, - XBoxOne_Reserved2 = 144, - XBoxOne_Reserved3 = 145, - XBoxOne_Reserved4 = 146, - XBoxOne_Reserved5 = 147, + XBoxOne_LeftGrip_Lower = 143, + XBoxOne_LeftGrip_Upper = 144, + XBoxOne_RightGrip_Lower = 145, + XBoxOne_RightGrip_Upper = 146, + XBoxOne_Share = 147, XBoxOne_Reserved6 = 148, XBoxOne_Reserved7 = 149, XBoxOne_Reserved8 = 150, @@ -1373,17 +1302,165 @@ namespace Steamworks Switch_LeftGrip_Upper = 245, Switch_RightGrip_Lower = 246, Switch_RightGrip_Upper = 247, - Switch_Reserved11 = 248, - Switch_Reserved12 = 249, - Switch_Reserved13 = 250, - Switch_Reserved14 = 251, + Switch_JoyConButton_N = 248, + Switch_JoyConButton_E = 249, + Switch_JoyConButton_S = 250, + Switch_JoyConButton_W = 251, Switch_Reserved15 = 252, Switch_Reserved16 = 253, Switch_Reserved17 = 254, Switch_Reserved18 = 255, Switch_Reserved19 = 256, Switch_Reserved20 = 257, - Count = 258, + PS5_X = 258, + PS5_Circle = 259, + PS5_Triangle = 260, + PS5_Square = 261, + PS5_LeftBumper = 262, + PS5_RightBumper = 263, + PS5_Option = 264, + PS5_Create = 265, + PS5_Mute = 266, + PS5_LeftPad_Touch = 267, + PS5_LeftPad_Swipe = 268, + PS5_LeftPad_Click = 269, + PS5_LeftPad_DPadNorth = 270, + PS5_LeftPad_DPadSouth = 271, + PS5_LeftPad_DPadWest = 272, + PS5_LeftPad_DPadEast = 273, + PS5_RightPad_Touch = 274, + PS5_RightPad_Swipe = 275, + PS5_RightPad_Click = 276, + PS5_RightPad_DPadNorth = 277, + PS5_RightPad_DPadSouth = 278, + PS5_RightPad_DPadWest = 279, + PS5_RightPad_DPadEast = 280, + PS5_CenterPad_Touch = 281, + PS5_CenterPad_Swipe = 282, + PS5_CenterPad_Click = 283, + PS5_CenterPad_DPadNorth = 284, + PS5_CenterPad_DPadSouth = 285, + PS5_CenterPad_DPadWest = 286, + PS5_CenterPad_DPadEast = 287, + PS5_LeftTrigger_Pull = 288, + PS5_LeftTrigger_Click = 289, + PS5_RightTrigger_Pull = 290, + PS5_RightTrigger_Click = 291, + PS5_LeftStick_Move = 292, + PS5_LeftStick_Click = 293, + PS5_LeftStick_DPadNorth = 294, + PS5_LeftStick_DPadSouth = 295, + PS5_LeftStick_DPadWest = 296, + PS5_LeftStick_DPadEast = 297, + PS5_RightStick_Move = 298, + PS5_RightStick_Click = 299, + PS5_RightStick_DPadNorth = 300, + PS5_RightStick_DPadSouth = 301, + PS5_RightStick_DPadWest = 302, + PS5_RightStick_DPadEast = 303, + PS5_DPad_North = 304, + PS5_DPad_South = 305, + PS5_DPad_West = 306, + PS5_DPad_East = 307, + PS5_Gyro_Move = 308, + PS5_Gyro_Pitch = 309, + PS5_Gyro_Yaw = 310, + PS5_Gyro_Roll = 311, + PS5_DPad_Move = 312, + PS5_LeftGrip = 313, + PS5_RightGrip = 314, + PS5_LeftFn = 315, + PS5_RightFn = 316, + PS5_Reserved5 = 317, + PS5_Reserved6 = 318, + PS5_Reserved7 = 319, + PS5_Reserved8 = 320, + PS5_Reserved9 = 321, + PS5_Reserved10 = 322, + PS5_Reserved11 = 323, + PS5_Reserved12 = 324, + PS5_Reserved13 = 325, + PS5_Reserved14 = 326, + PS5_Reserved15 = 327, + PS5_Reserved16 = 328, + PS5_Reserved17 = 329, + PS5_Reserved18 = 330, + PS5_Reserved19 = 331, + PS5_Reserved20 = 332, + SteamDeck_A = 333, + SteamDeck_B = 334, + SteamDeck_X = 335, + SteamDeck_Y = 336, + SteamDeck_L1 = 337, + SteamDeck_R1 = 338, + SteamDeck_Menu = 339, + SteamDeck_View = 340, + SteamDeck_LeftPad_Touch = 341, + SteamDeck_LeftPad_Swipe = 342, + SteamDeck_LeftPad_Click = 343, + SteamDeck_LeftPad_DPadNorth = 344, + SteamDeck_LeftPad_DPadSouth = 345, + SteamDeck_LeftPad_DPadWest = 346, + SteamDeck_LeftPad_DPadEast = 347, + SteamDeck_RightPad_Touch = 348, + SteamDeck_RightPad_Swipe = 349, + SteamDeck_RightPad_Click = 350, + SteamDeck_RightPad_DPadNorth = 351, + SteamDeck_RightPad_DPadSouth = 352, + SteamDeck_RightPad_DPadWest = 353, + SteamDeck_RightPad_DPadEast = 354, + SteamDeck_L2_SoftPull = 355, + SteamDeck_L2 = 356, + SteamDeck_R2_SoftPull = 357, + SteamDeck_R2 = 358, + SteamDeck_LeftStick_Move = 359, + SteamDeck_L3 = 360, + SteamDeck_LeftStick_DPadNorth = 361, + SteamDeck_LeftStick_DPadSouth = 362, + SteamDeck_LeftStick_DPadWest = 363, + SteamDeck_LeftStick_DPadEast = 364, + SteamDeck_LeftStick_Touch = 365, + SteamDeck_RightStick_Move = 366, + SteamDeck_R3 = 367, + SteamDeck_RightStick_DPadNorth = 368, + SteamDeck_RightStick_DPadSouth = 369, + SteamDeck_RightStick_DPadWest = 370, + SteamDeck_RightStick_DPadEast = 371, + SteamDeck_RightStick_Touch = 372, + SteamDeck_L4 = 373, + SteamDeck_R4 = 374, + SteamDeck_L5 = 375, + SteamDeck_R5 = 376, + SteamDeck_DPad_Move = 377, + SteamDeck_DPad_North = 378, + SteamDeck_DPad_South = 379, + SteamDeck_DPad_West = 380, + SteamDeck_DPad_East = 381, + SteamDeck_Gyro_Move = 382, + SteamDeck_Gyro_Pitch = 383, + SteamDeck_Gyro_Yaw = 384, + SteamDeck_Gyro_Roll = 385, + SteamDeck_Reserved1 = 386, + SteamDeck_Reserved2 = 387, + SteamDeck_Reserved3 = 388, + SteamDeck_Reserved4 = 389, + SteamDeck_Reserved5 = 390, + SteamDeck_Reserved6 = 391, + SteamDeck_Reserved7 = 392, + SteamDeck_Reserved8 = 393, + SteamDeck_Reserved9 = 394, + SteamDeck_Reserved10 = 395, + SteamDeck_Reserved11 = 396, + SteamDeck_Reserved12 = 397, + SteamDeck_Reserved13 = 398, + SteamDeck_Reserved14 = 399, + SteamDeck_Reserved15 = 400, + SteamDeck_Reserved16 = 401, + SteamDeck_Reserved17 = 402, + SteamDeck_Reserved18 = 403, + SteamDeck_Reserved19 = 404, + SteamDeck_Reserved20 = 405, + Count = 406, MaximumPossibleValue = 32767, } @@ -1432,6 +1509,26 @@ namespace Steamworks Right = 1, } + // + // EControllerHapticLocation + // + internal enum ControllerHapticLocation : int + { + Left = 1, + Right = 2, + Both = 3, + } + + // + // EControllerHapticType + // + internal enum ControllerHapticType : int + { + Off = 0, + Tick = 1, + Click = 2, + } + // // ESteamInputType // @@ -1450,10 +1547,24 @@ namespace Steamworks SwitchProController = 10, MobileTouch = 11, PS3Controller = 12, - Count = 13, + PS5Controller = 13, + SteamDeckController = 14, + Count = 15, MaximumPossibleValue = 255, } + // + // ESteamInputConfigurationEnableType + // + internal enum SteamInputConfigurationEnableType : int + { + None = 0, + Playstation = 1, + Xbox = 2, + Generic = 4, + Switch = 8, + } + // // ESteamInputLEDFlag // @@ -1463,6 +1574,38 @@ namespace Steamworks RestoreUserDefault = 1, } + // + // ESteamInputGlyphSize + // + public enum GlyphSize : int + { + Small = 0, + Medium = 1, + Large = 2, + Count = 3, + } + + // + // ESteamInputGlyphStyle + // + internal enum SteamInputGlyphStyle : int + { + Knockout = 0, + Light = 1, + Dark = 2, + NeutralColorABXY = 16, + SolidABXY = 32, + } + + // + // ESteamInputActionEventType + // + internal enum SteamInputActionEventType : int + { + DigitalAction = 0, + AnalogAction = 1, + } + // // EControllerActionOrigin // @@ -1713,7 +1856,148 @@ namespace Steamworks XBoxOne_DPad_Move = 242, XBox360_DPad_Move = 243, Switch_DPad_Move = 244, - Count = 245, + PS5_X = 245, + PS5_Circle = 246, + PS5_Triangle = 247, + PS5_Square = 248, + PS5_LeftBumper = 249, + PS5_RightBumper = 250, + PS5_Option = 251, + PS5_Create = 252, + PS5_Mute = 253, + PS5_LeftPad_Touch = 254, + PS5_LeftPad_Swipe = 255, + PS5_LeftPad_Click = 256, + PS5_LeftPad_DPadNorth = 257, + PS5_LeftPad_DPadSouth = 258, + PS5_LeftPad_DPadWest = 259, + PS5_LeftPad_DPadEast = 260, + PS5_RightPad_Touch = 261, + PS5_RightPad_Swipe = 262, + PS5_RightPad_Click = 263, + PS5_RightPad_DPadNorth = 264, + PS5_RightPad_DPadSouth = 265, + PS5_RightPad_DPadWest = 266, + PS5_RightPad_DPadEast = 267, + PS5_CenterPad_Touch = 268, + PS5_CenterPad_Swipe = 269, + PS5_CenterPad_Click = 270, + PS5_CenterPad_DPadNorth = 271, + PS5_CenterPad_DPadSouth = 272, + PS5_CenterPad_DPadWest = 273, + PS5_CenterPad_DPadEast = 274, + PS5_LeftTrigger_Pull = 275, + PS5_LeftTrigger_Click = 276, + PS5_RightTrigger_Pull = 277, + PS5_RightTrigger_Click = 278, + PS5_LeftStick_Move = 279, + PS5_LeftStick_Click = 280, + PS5_LeftStick_DPadNorth = 281, + PS5_LeftStick_DPadSouth = 282, + PS5_LeftStick_DPadWest = 283, + PS5_LeftStick_DPadEast = 284, + PS5_RightStick_Move = 285, + PS5_RightStick_Click = 286, + PS5_RightStick_DPadNorth = 287, + PS5_RightStick_DPadSouth = 288, + PS5_RightStick_DPadWest = 289, + PS5_RightStick_DPadEast = 290, + PS5_DPad_Move = 291, + PS5_DPad_North = 292, + PS5_DPad_South = 293, + PS5_DPad_West = 294, + PS5_DPad_East = 295, + PS5_Gyro_Move = 296, + PS5_Gyro_Pitch = 297, + PS5_Gyro_Yaw = 298, + PS5_Gyro_Roll = 299, + XBoxOne_LeftGrip_Lower = 300, + XBoxOne_LeftGrip_Upper = 301, + XBoxOne_RightGrip_Lower = 302, + XBoxOne_RightGrip_Upper = 303, + XBoxOne_Share = 304, + SteamDeck_A = 305, + SteamDeck_B = 306, + SteamDeck_X = 307, + SteamDeck_Y = 308, + SteamDeck_L1 = 309, + SteamDeck_R1 = 310, + SteamDeck_Menu = 311, + SteamDeck_View = 312, + SteamDeck_LeftPad_Touch = 313, + SteamDeck_LeftPad_Swipe = 314, + SteamDeck_LeftPad_Click = 315, + SteamDeck_LeftPad_DPadNorth = 316, + SteamDeck_LeftPad_DPadSouth = 317, + SteamDeck_LeftPad_DPadWest = 318, + SteamDeck_LeftPad_DPadEast = 319, + SteamDeck_RightPad_Touch = 320, + SteamDeck_RightPad_Swipe = 321, + SteamDeck_RightPad_Click = 322, + SteamDeck_RightPad_DPadNorth = 323, + SteamDeck_RightPad_DPadSouth = 324, + SteamDeck_RightPad_DPadWest = 325, + SteamDeck_RightPad_DPadEast = 326, + SteamDeck_L2_SoftPull = 327, + SteamDeck_L2 = 328, + SteamDeck_R2_SoftPull = 329, + SteamDeck_R2 = 330, + SteamDeck_LeftStick_Move = 331, + SteamDeck_L3 = 332, + SteamDeck_LeftStick_DPadNorth = 333, + SteamDeck_LeftStick_DPadSouth = 334, + SteamDeck_LeftStick_DPadWest = 335, + SteamDeck_LeftStick_DPadEast = 336, + SteamDeck_LeftStick_Touch = 337, + SteamDeck_RightStick_Move = 338, + SteamDeck_R3 = 339, + SteamDeck_RightStick_DPadNorth = 340, + SteamDeck_RightStick_DPadSouth = 341, + SteamDeck_RightStick_DPadWest = 342, + SteamDeck_RightStick_DPadEast = 343, + SteamDeck_RightStick_Touch = 344, + SteamDeck_L4 = 345, + SteamDeck_R4 = 346, + SteamDeck_L5 = 347, + SteamDeck_R5 = 348, + SteamDeck_DPad_Move = 349, + SteamDeck_DPad_North = 350, + SteamDeck_DPad_South = 351, + SteamDeck_DPad_West = 352, + SteamDeck_DPad_East = 353, + SteamDeck_Gyro_Move = 354, + SteamDeck_Gyro_Pitch = 355, + SteamDeck_Gyro_Yaw = 356, + SteamDeck_Gyro_Roll = 357, + SteamDeck_Reserved1 = 358, + SteamDeck_Reserved2 = 359, + SteamDeck_Reserved3 = 360, + SteamDeck_Reserved4 = 361, + SteamDeck_Reserved5 = 362, + SteamDeck_Reserved6 = 363, + SteamDeck_Reserved7 = 364, + SteamDeck_Reserved8 = 365, + SteamDeck_Reserved9 = 366, + SteamDeck_Reserved10 = 367, + SteamDeck_Reserved11 = 368, + SteamDeck_Reserved12 = 369, + SteamDeck_Reserved13 = 370, + SteamDeck_Reserved14 = 371, + SteamDeck_Reserved15 = 372, + SteamDeck_Reserved16 = 373, + SteamDeck_Reserved17 = 374, + SteamDeck_Reserved18 = 375, + SteamDeck_Reserved19 = 376, + SteamDeck_Reserved20 = 377, + Switch_JoyConButton_N = 378, + Switch_JoyConButton_E = 379, + Switch_JoyConButton_S = 380, + Switch_JoyConButton_W = 381, + PS5_LeftGrip = 382, + PS5_RightGrip = 383, + PS5_LeftFn = 384, + PS5_RightFn = 385, + Count = 386, MaximumPossibleValue = 32767, } @@ -1801,6 +2085,7 @@ namespace Steamworks RankedByLifetimeAveragePlaytime = 16, RankedByPlaytimeSessionsTrend = 17, RankedByLifetimePlaytimeSessions = 18, + RankedByLastUpdatedDate = 19, } // @@ -1853,7 +2138,7 @@ namespace Steamworks // // EItemPreviewType // - internal enum ItemPreviewType : int + public enum ItemPreviewType : int { Image = 0, YouTubeVideo = 1, @@ -1863,6 +2148,18 @@ namespace Steamworks ReservedMax = 255, } + // + // EUGCContentDescriptorID + // + internal enum UGCContentDescriptorID : int + { + NudityOrSexualContent = 1, + FrequentViolenceOrGore = 2, + AdultOnlySexualContent = 3, + GratuitousSexualContent = 4, + AnyMatureContent = 5, + } + // // ESteamItemFlags // @@ -1873,17 +2170,6 @@ namespace Steamworks Consumed = 512, } - // - // ESteamTVRegionBehavior - // - internal enum SteamTVRegionBehavior : int - { - Invalid = -1, - Hover = 0, - ClickPopup = 1, - ClickSurroundingRegion = 2, - } - // // EParentalFeature // @@ -1903,7 +2189,8 @@ namespace Steamworks Library = 11, Test = 12, SiteLicense = 13, - Max = 14, + KioskMode = 14, + Max = 15, } // @@ -1943,6 +2230,8 @@ namespace Steamworks Invalid = 0, SteamID = 16, XboxPairwiseID = 17, + SonyPSN = 18, + GoogleStadia = 19, IPAddress = 1, GenericString = 2, GenericBytes = 3, @@ -1950,6 +2239,17 @@ namespace Steamworks Force32bit = 2147483647, } + // + // ESteamNetworkingFakeIPType + // + internal enum SteamNetworkingFakeIPType : int + { + Invalid = 0, + NotFake = 1, + GlobalIPv4 = 2, + LocalIPv4 = 3, + } + // // ESteamNetworkingConnectionState // @@ -1984,22 +2284,24 @@ namespace Steamworks Local_HostedServerPrimaryRelay = 3003, Local_NetworkConfig = 3004, Local_Rights = 3005, + Local_P2P_ICE_NoPublicAddresses = 3006, Local_Max = 3999, Remote_Min = 4000, Remote_Timeout = 4001, Remote_BadCrypt = 4002, Remote_BadCert = 4003, - Remote_NotLoggedIn = 4004, - Remote_NotRunningApp = 4005, Remote_BadProtocolVersion = 4006, + Remote_P2P_ICE_NoPublicAddresses = 4007, Remote_Max = 4999, Misc_Min = 5000, Misc_Generic = 5001, Misc_InternalError = 5002, Misc_Timeout = 5003, - Misc_RelayConnectivity = 5004, Misc_SteamConnectivity = 5005, Misc_NoRelaySessionsToClient = 5006, + Misc_P2P_Rendezvous = 5008, + Misc_P2P_NAT_Firewall = 5009, + Misc_PeerSentNoConnection = 5010, Misc_Max = 5999, } @@ -2023,7 +2325,7 @@ namespace Steamworks Int64 = 2, Float = 3, String = 4, - FunctionPtr = 5, + Ptr = 5, } // @@ -2032,6 +2334,21 @@ namespace Steamworks internal enum NetConfig : int { Invalid = 0, + TimeoutInitial = 24, + TimeoutConnected = 25, + SendBufferSize = 9, + ConnectionUserData = 40, + SendRateMin = 10, + SendRateMax = 11, + NagleTime = 12, + IP_AllowWithoutAuth = 23, + MTU_PacketSize = 32, + MTU_DataSize = 33, + Unencrypted = 34, + SymmetricConnect = 37, + LocalVirtualPort = 38, + DualWifi_Enable = 39, + EnableDiagnosticsUI = 46, FakePacketLoss_Send = 2, FakePacketLoss_Recv = 3, FakePacketLag_Send = 4, @@ -2042,17 +2359,26 @@ namespace Steamworks FakePacketDup_Send = 26, FakePacketDup_Recv = 27, FakePacketDup_TimeMax = 28, - TimeoutInitial = 24, - TimeoutConnected = 25, - SendBufferSize = 9, - SendRateMin = 10, - SendRateMax = 11, - NagleTime = 12, - IP_AllowWithoutAuth = 23, - MTU_PacketSize = 32, - MTU_DataSize = 33, - Unencrypted = 34, - EnumerateDevVars = 35, + PacketTraceMaxBytes = 41, + FakeRateLimit_Send_Rate = 42, + FakeRateLimit_Send_Burst = 43, + FakeRateLimit_Recv_Rate = 44, + FakeRateLimit_Recv_Burst = 45, + Callback_ConnectionStatusChanged = 201, + Callback_AuthStatusChanged = 202, + Callback_RelayNetworkStatusChanged = 203, + Callback_MessagesSessionRequest = 204, + Callback_MessagesSessionFailed = 205, + Callback_CreateConnectionSignaling = 206, + Callback_FakeIPResult = 207, + P2P_STUN_ServerList = 103, + P2P_Transport_ICE_Enable = 104, + P2P_Transport_ICE_Penalty = 105, + P2P_Transport_SDR_Penalty = 106, + P2P_TURN_ServerList = 107, + P2P_TURN_UserList = 108, + P2P_TURN_PassList = 109, + P2P_Transport_ICE_Implementation = 110, SDRClient_ConsecutitivePingTimeoutsFailInitial = 19, SDRClient_ConsecutitivePingTimeoutsFail = 20, SDRClient_MinPingsBeforePingAccurate = 21, @@ -2067,6 +2393,7 @@ namespace Steamworks LogLevel_PacketGaps = 16, LogLevel_P2PRendezvous = 17, LogLevel_SDRRelayPings = 18, + DELETED_EnumerateDevVars = 35, } // diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs b/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs index 3f802a060..0ce830172 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs @@ -88,6 +88,25 @@ namespace Steamworks.Data } + internal partial struct NetKeyValue + { + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetInt32", CallingConvention = Platform.CC)] + internal static extern void InternalSetInt32( ref NetKeyValue self, NetConfig eVal, int data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetInt64", CallingConvention = Platform.CC)] + internal static extern void InternalSetInt64( ref NetKeyValue self, NetConfig eVal, long data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetFloat", CallingConvention = Platform.CC)] + internal static extern void InternalSetFloat( ref NetKeyValue self, NetConfig eVal, float data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetPtr", CallingConvention = Platform.CC)] + internal static extern void InternalSetPtr( ref NetKeyValue self, NetConfig eVal, IntPtr data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetString", CallingConvention = Platform.CC)] + internal static extern void InternalSetString( ref NetKeyValue self, NetConfig eVal, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string data ); + + } + public partial struct NetIdentity { [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_Clear", CallingConvention = Platform.CC)] @@ -116,12 +135,37 @@ namespace Steamworks.Data [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetXboxPairwiseID", CallingConvention = Platform.CC)] internal static extern Utf8StringPointer InternalGetXboxPairwiseID( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetPSNID", CallingConvention = Platform.CC)] + internal static extern void InternalSetPSNID( ref NetIdentity self, ulong id ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetPSNID", CallingConvention = Platform.CC)] + internal static extern ulong InternalGetPSNID( ref NetIdentity self ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetStadiaID", CallingConvention = Platform.CC)] + internal static extern void InternalSetStadiaID( ref NetIdentity self, ulong id ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetStadiaID", CallingConvention = Platform.CC)] + internal static extern ulong InternalGetStadiaID( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetIPAddr", CallingConvention = Platform.CC)] internal static extern void InternalSetIPAddr( ref NetIdentity self, ref NetAddress addr ); [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetIPAddr", CallingConvention = Platform.CC)] internal static extern IntPtr InternalGetIPAddr( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetIPv4Addr", CallingConvention = Platform.CC)] + internal static extern void InternalSetIPv4Addr( ref NetIdentity self, uint nIPv4, ushort nPort ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetIPv4", CallingConvention = Platform.CC)] + internal static extern uint InternalGetIPv4( ref NetIdentity self ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetFakeIPType", CallingConvention = Platform.CC)] + internal static extern SteamNetworkingFakeIPType InternalGetFakeIPType( ref NetIdentity self ); + + [return: MarshalAs( UnmanagedType.I1 )] + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_IsFakeIP", CallingConvention = Platform.CC)] + internal static extern bool InternalIsFakeIP( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetLocalHost", CallingConvention = Platform.CC)] internal static extern void InternalSetLocalHost( ref NetIdentity self ); @@ -196,6 +240,13 @@ namespace Steamworks.Data [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIPAddr_IsEqualTo", CallingConvention = Platform.CC)] internal static extern bool InternalIsEqualTo( ref NetAddress self, ref NetAddress x ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIPAddr_GetFakeIPType", CallingConvention = Platform.CC)] + internal static extern SteamNetworkingFakeIPType InternalGetFakeIPType( ref NetAddress self ); + + [return: MarshalAs( UnmanagedType.I1 )] + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIPAddr_IsFakeIP", CallingConvention = Platform.CC)] + internal static extern bool InternalIsFakeIP( ref NetAddress self ); + } internal partial struct NetMsg diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs b/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs index e3bc0a785..8724de68a 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs @@ -17,14 +17,6 @@ namespace Steamworks.Data } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct FriendSessionStateInfo_t - { - internal uint IOnlineSessionInstances; // m_uiOnlineSessionInstances uint32 - internal byte IPublishedToFriendsSessionInstance; // m_uiPublishedToFriendsSessionInstance uint8 - - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal partial struct servernetadr_t { @@ -113,6 +105,39 @@ namespace Steamworks.Data } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct InputMotionDataV2_t + { + internal float DriftCorrectedQuatX; // driftCorrectedQuatX float + internal float DriftCorrectedQuatY; // driftCorrectedQuatY float + internal float DriftCorrectedQuatZ; // driftCorrectedQuatZ float + internal float DriftCorrectedQuatW; // driftCorrectedQuatW float + internal float SensorFusionQuatX; // sensorFusionQuatX float + internal float SensorFusionQuatY; // sensorFusionQuatY float + internal float SensorFusionQuatZ; // sensorFusionQuatZ float + internal float SensorFusionQuatW; // sensorFusionQuatW float + internal float DeferredSensorFusionQuatX; // deferredSensorFusionQuatX float + internal float DeferredSensorFusionQuatY; // deferredSensorFusionQuatY float + internal float DeferredSensorFusionQuatZ; // deferredSensorFusionQuatZ float + internal float DeferredSensorFusionQuatW; // deferredSensorFusionQuatW float + internal float GravityX; // gravityX float + internal float GravityY; // gravityY float + internal float GravityZ; // gravityZ float + internal float DegreesPerSecondX; // degreesPerSecondX float + internal float DegreesPerSecondY; // degreesPerSecondY float + internal float DegreesPerSecondZ; // degreesPerSecondZ float + + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputActionEvent_t + { + internal ulong ControllerHandle; // controllerHandle InputHandle_t + internal SteamInputActionEventType EEventType; // eEventType ESteamInputActionEventType + // internal SteamInputActionEvent_t.AnalogAction_t AnalogAction; // analogAction SteamInputActionEvent_t::AnalogAction_t + + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamUGCDetails_t { @@ -168,37 +193,6 @@ namespace Steamworks.Data } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct SteamTVRegion_t - { - internal uint UnMinX; // unMinX uint32 - internal uint UnMinY; // unMinY uint32 - internal uint UnMaxX; // unMaxX uint32 - internal uint UnMaxY; // unMaxY uint32 - - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct SteamNetworkingQuickConnectionStatus - { - internal ConnectionState State; // m_eState ESteamNetworkingConnectionState - internal int Ping; // m_nPing int - internal float ConnectionQualityLocal; // m_flConnectionQualityLocal float - internal float ConnectionQualityRemote; // m_flConnectionQualityRemote float - internal float OutPacketsPerSec; // m_flOutPacketsPerSec float - internal float OutBytesPerSec; // m_flOutBytesPerSec float - internal float InPacketsPerSec; // m_flInPacketsPerSec float - internal float InBytesPerSec; // m_flInBytesPerSec float - internal int SendRateBytesPerSecond; // m_nSendRateBytesPerSecond int - internal int CbPendingUnreliable; // m_cbPendingUnreliable int - internal int CbPendingReliable; // m_cbPendingReliable int - internal int CbSentUnackedReliable; // m_cbSentUnackedReliable int - internal long EcQueueTime; // m_usecQueueTime SteamNetworkingMicroseconds - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16, ArraySubType = UnmanagedType.U4)] - internal uint[] Reserved; // reserved uint32 [16] - - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal partial struct SteamDatagramHostedAddress { diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs b/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs index 44f5be6e3..5f5085f04 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs @@ -6,118 +6,6 @@ using System.Threading.Tasks; namespace Steamworks.Data { - internal struct GID_t : IEquatable, IComparable - { - // Name: GID_t, Type: unsigned long long - public ulong Value; - - public static implicit operator GID_t( ulong value ) => new GID_t(){ Value = value }; - public static implicit operator ulong( GID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (GID_t) p ); - public bool Equals( GID_t p ) => p.Value == Value; - public static bool operator ==( GID_t a, GID_t b ) => a.Equals( b ); - public static bool operator !=( GID_t a, GID_t b ) => !a.Equals( b ); - public int CompareTo( GID_t other ) => Value.CompareTo( other.Value ); - } - - internal struct JobID_t : IEquatable, IComparable - { - // Name: JobID_t, Type: unsigned long long - public ulong Value; - - public static implicit operator JobID_t( ulong value ) => new JobID_t(){ Value = value }; - public static implicit operator ulong( JobID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (JobID_t) p ); - public bool Equals( JobID_t p ) => p.Value == Value; - public static bool operator ==( JobID_t a, JobID_t b ) => a.Equals( b ); - public static bool operator !=( JobID_t a, JobID_t b ) => !a.Equals( b ); - public int CompareTo( JobID_t other ) => Value.CompareTo( other.Value ); - } - - internal struct TxnID_t : IEquatable, IComparable - { - // Name: TxnID_t, Type: unsigned long long - public ulong Value; - - public static implicit operator TxnID_t( ulong value ) => new TxnID_t(){ Value = value }; - public static implicit operator ulong( TxnID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (TxnID_t) p ); - public bool Equals( TxnID_t p ) => p.Value == Value; - public static bool operator ==( TxnID_t a, TxnID_t b ) => a.Equals( b ); - public static bool operator !=( TxnID_t a, TxnID_t b ) => !a.Equals( b ); - public int CompareTo( TxnID_t other ) => Value.CompareTo( other.Value ); - } - - internal struct PackageId_t : IEquatable, IComparable - { - // Name: PackageId_t, Type: unsigned int - public uint Value; - - public static implicit operator PackageId_t( uint value ) => new PackageId_t(){ Value = value }; - public static implicit operator uint( PackageId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (PackageId_t) p ); - public bool Equals( PackageId_t p ) => p.Value == Value; - public static bool operator ==( PackageId_t a, PackageId_t b ) => a.Equals( b ); - public static bool operator !=( PackageId_t a, PackageId_t b ) => !a.Equals( b ); - public int CompareTo( PackageId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct BundleId_t : IEquatable, IComparable - { - // Name: BundleId_t, Type: unsigned int - public uint Value; - - public static implicit operator BundleId_t( uint value ) => new BundleId_t(){ Value = value }; - public static implicit operator uint( BundleId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (BundleId_t) p ); - public bool Equals( BundleId_t p ) => p.Value == Value; - public static bool operator ==( BundleId_t a, BundleId_t b ) => a.Equals( b ); - public static bool operator !=( BundleId_t a, BundleId_t b ) => !a.Equals( b ); - public int CompareTo( BundleId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct AssetClassId_t : IEquatable, IComparable - { - // Name: AssetClassId_t, Type: unsigned long long - public ulong Value; - - public static implicit operator AssetClassId_t( ulong value ) => new AssetClassId_t(){ Value = value }; - public static implicit operator ulong( AssetClassId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (AssetClassId_t) p ); - public bool Equals( AssetClassId_t p ) => p.Value == Value; - public static bool operator ==( AssetClassId_t a, AssetClassId_t b ) => a.Equals( b ); - public static bool operator !=( AssetClassId_t a, AssetClassId_t b ) => !a.Equals( b ); - public int CompareTo( AssetClassId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct PhysicalItemId_t : IEquatable, IComparable - { - // Name: PhysicalItemId_t, Type: unsigned int - public uint Value; - - public static implicit operator PhysicalItemId_t( uint value ) => new PhysicalItemId_t(){ Value = value }; - public static implicit operator uint( PhysicalItemId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (PhysicalItemId_t) p ); - public bool Equals( PhysicalItemId_t p ) => p.Value == Value; - public static bool operator ==( PhysicalItemId_t a, PhysicalItemId_t b ) => a.Equals( b ); - public static bool operator !=( PhysicalItemId_t a, PhysicalItemId_t b ) => !a.Equals( b ); - public int CompareTo( PhysicalItemId_t other ) => Value.CompareTo( other.Value ); - } - internal struct DepotId_t : IEquatable, IComparable { // Name: DepotId_t, Type: unsigned int @@ -150,22 +38,6 @@ namespace Steamworks.Data public int CompareTo( RTime32 other ) => Value.CompareTo( other.Value ); } - internal struct CellID_t : IEquatable, IComparable - { - // Name: CellID_t, Type: unsigned int - public uint Value; - - public static implicit operator CellID_t( uint value ) => new CellID_t(){ Value = value }; - public static implicit operator uint( CellID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (CellID_t) p ); - public bool Equals( CellID_t p ) => p.Value == Value; - public static bool operator ==( CellID_t a, CellID_t b ) => a.Equals( b ); - public static bool operator !=( CellID_t a, CellID_t b ) => !a.Equals( b ); - public int CompareTo( CellID_t other ) => Value.CompareTo( other.Value ); - } - internal struct SteamAPICall_t : IEquatable, IComparable { // Name: SteamAPICall_t, Type: unsigned long long @@ -198,54 +70,6 @@ namespace Steamworks.Data public int CompareTo( AccountID_t other ) => Value.CompareTo( other.Value ); } - internal struct PartnerId_t : IEquatable, IComparable - { - // Name: PartnerId_t, Type: unsigned int - public uint Value; - - public static implicit operator PartnerId_t( uint value ) => new PartnerId_t(){ Value = value }; - public static implicit operator uint( PartnerId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (PartnerId_t) p ); - public bool Equals( PartnerId_t p ) => p.Value == Value; - public static bool operator ==( PartnerId_t a, PartnerId_t b ) => a.Equals( b ); - public static bool operator !=( PartnerId_t a, PartnerId_t b ) => !a.Equals( b ); - public int CompareTo( PartnerId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct ManifestId_t : IEquatable, IComparable - { - // Name: ManifestId_t, Type: unsigned long long - public ulong Value; - - public static implicit operator ManifestId_t( ulong value ) => new ManifestId_t(){ Value = value }; - public static implicit operator ulong( ManifestId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (ManifestId_t) p ); - public bool Equals( ManifestId_t p ) => p.Value == Value; - public static bool operator ==( ManifestId_t a, ManifestId_t b ) => a.Equals( b ); - public static bool operator !=( ManifestId_t a, ManifestId_t b ) => !a.Equals( b ); - public int CompareTo( ManifestId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct SiteId_t : IEquatable, IComparable - { - // Name: SiteId_t, Type: unsigned long long - public ulong Value; - - public static implicit operator SiteId_t( ulong value ) => new SiteId_t(){ Value = value }; - public static implicit operator ulong( SiteId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (SiteId_t) p ); - public bool Equals( SiteId_t p ) => p.Value == Value; - public static bool operator ==( SiteId_t a, SiteId_t b ) => a.Equals( b ); - public static bool operator !=( SiteId_t a, SiteId_t b ) => !a.Equals( b ); - public int CompareTo( SiteId_t other ) => Value.CompareTo( other.Value ); - } - internal struct PartyBeaconID_t : IEquatable, IComparable { // Name: PartyBeaconID_t, Type: unsigned long long @@ -278,22 +102,6 @@ namespace Steamworks.Data public int CompareTo( HAuthTicket other ) => Value.CompareTo( other.Value ); } - internal struct BREAKPAD_HANDLE : IEquatable, IComparable - { - // Name: BREAKPAD_HANDLE, Type: void * - public IntPtr Value; - - public static implicit operator BREAKPAD_HANDLE( IntPtr value ) => new BREAKPAD_HANDLE(){ Value = value }; - public static implicit operator IntPtr( BREAKPAD_HANDLE value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (BREAKPAD_HANDLE) p ); - public bool Equals( BREAKPAD_HANDLE p ) => p.Value == Value; - public static bool operator ==( BREAKPAD_HANDLE a, BREAKPAD_HANDLE b ) => a.Equals( b ); - public static bool operator !=( BREAKPAD_HANDLE a, BREAKPAD_HANDLE b ) => !a.Equals( b ); - public int CompareTo( BREAKPAD_HANDLE other ) => Value.ToInt64().CompareTo( other.Value.ToInt64() ); - } - internal struct HSteamPipe : IEquatable, IComparable { // Name: HSteamPipe, Type: int diff --git a/Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs b/Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs new file mode 100644 index 000000000..ba0ccc2b9 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Steamworks.Data; + +namespace Steamworks +{ + internal static unsafe class BufferManager + { + private sealed class ReferenceCounter + { + public IntPtr Pointer { get; private set; } + public int Size { get; private set; } + private int _count; + + public void Set( IntPtr ptr, int size, int referenceCount ) + { + if ( ptr == IntPtr.Zero ) + throw new ArgumentNullException( nameof( ptr ) ); + if ( size <= 0 ) + throw new ArgumentOutOfRangeException( nameof( size ) ); + if ( referenceCount <= 0 ) + throw new ArgumentOutOfRangeException( nameof( referenceCount ) ); + + Pointer = ptr; + Size = size; + + var prevCount = Interlocked.Exchange(ref _count, referenceCount); + if (prevCount != 0) + { +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Warning, $"{nameof( BufferManager )} set reference count when current count was not 0" ); +#endif + } + } + + public bool Decrement() + { + var newCount = Interlocked.Decrement( ref _count ); + if ( newCount < 0 ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, $"Prevented double free of {nameof(BufferManager)} pointer" ); + return false; + } + + return newCount == 0; + } + } + + [UnmanagedFunctionPointer( CallingConvention.Cdecl )] + private delegate void FreeFn( NetMsg* msg ); + + private static readonly Stack ReferenceCounterPool = + new Stack( 1024 ); + + private static readonly Dictionary> BufferPools = + new Dictionary>(); + + private static readonly Dictionary ReferenceCounters = + new Dictionary( 1024 ); + + private static readonly FreeFn FreeFunctionPin = new FreeFn( Free ); + + public static readonly IntPtr FreeFunctionPointer = Marshal.GetFunctionPointerForDelegate( FreeFunctionPin ); + + public static IntPtr Get( int size, int referenceCount ) + { + const int maxSize = 16 * 1024 * 1024; + if ( size < 0 || size > maxSize ) + throw new ArgumentOutOfRangeException( nameof( size ) ); + if ( referenceCount <= 0 ) + throw new ArgumentOutOfRangeException( nameof( referenceCount ) ); + + AllocateBuffer( size, out var ptr, out var actualSize ); + var counter = AllocateReferenceCounter( ptr, actualSize, referenceCount ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated {ptr.ToInt64():X8} (size={size}, actualSize={actualSize}) with {referenceCount} references" ); +#endif + + lock ( ReferenceCounters ) + { + ReferenceCounters.Add( ptr, counter ); + } + + return ptr; + } + + [MonoPInvokeCallback] + private static void Free( NetMsg* msg ) + { + var ptr = msg->DataPtr; + + lock ( ReferenceCounters ) + { + if ( !ReferenceCounters.TryGetValue( ptr, out var counter ) ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, $"Attempt to free pointer not tracked by {nameof(BufferManager)}: {ptr.ToInt64():X8}" ); + return; + } + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, $"{nameof( BufferManager )} decrementing reference count of {ptr.ToInt64():X8}" ); +#endif + + if ( counter.Decrement() ) + { +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, $"{nameof( BufferManager )} freeing {ptr.ToInt64():X8} as it is now unreferenced" ); + + if ( ptr != counter.Pointer ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, + $"{nameof( BufferManager )} freed pointer ({ptr.ToInt64():X8}) does not match counter pointer ({counter.Pointer.ToInt64():X8})" ); + } + + var bucketSize = GetBucketSize( counter.Size ); + if ( counter.Size != bucketSize ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, + $"{nameof( BufferManager )} freed pointer size ({counter.Size}) does not match bucket size ({bucketSize})" ); + } +#endif + + ReferenceCounters.Remove( ptr ); + FreeBuffer( ptr, counter.Size ); + FreeReferenceCounter( counter ); + } + } + } + + private static ReferenceCounter AllocateReferenceCounter( IntPtr ptr, int size, int referenceCount ) + { + lock ( ReferenceCounterPool ) + { + var counter = ReferenceCounterPool.Count > 0 + ? ReferenceCounterPool.Pop() + : new ReferenceCounter(); + + counter.Set( ptr, size, referenceCount ); + return counter; + } + } + + private static void FreeReferenceCounter( ReferenceCounter counter ) + { + if ( counter == null ) + throw new ArgumentNullException( nameof( counter ) ); + + lock ( ReferenceCounterPool ) + { + if ( ReferenceCounterPool.Count >= 1024 ) + { + // we don't want to keep a ton of these lying around - let it GC if we have too many + return; + } + + ReferenceCounterPool.Push( counter ); + } + } + + private static void AllocateBuffer( int minimumSize, out IntPtr ptr, out int size ) + { + var bucketSize = GetBucketSize( minimumSize ); + + if ( bucketSize <= 0 ) + { + // not bucketed, no pooling for this size + ptr = Marshal.AllocHGlobal( minimumSize ); + size = minimumSize; + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated unpooled pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + lock ( BufferPools ) + { + if ( !BufferPools.TryGetValue( bucketSize, out var bucketPool ) || bucketPool.Count == 0 ) + { + // nothing pooled yet, but we can pool this size + ptr = Marshal.AllocHGlobal( bucketSize ); + size = bucketSize; + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated new poolable pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + ptr = bucketPool.Pop(); + size = bucketSize; +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated pointer from pool {ptr.ToInt64():X8} (size={size})" ); +#endif + } + } + + private static void FreeBuffer( IntPtr ptr, int size ) + { + var bucketSize = GetBucketSize( size ); + var bucketLimit = GetBucketLimit( size ); + + if ( bucketSize <= 0 || bucketLimit <= 0 ) + { + // not bucketed, no pooling for this size + Marshal.FreeHGlobal( ptr ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} freed unpooled pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + lock ( BufferPools ) + { + if ( !BufferPools.TryGetValue( bucketSize, out var bucketPool ) ) + { + bucketPool = new Stack( bucketLimit ); + BufferPools.Add( bucketSize, bucketPool ); + } + + if ( bucketPool.Count >= bucketLimit ) + { + // pool overflow, get rid + Marshal.FreeHGlobal( ptr ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} pool overflow, freed pooled pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + bucketPool.Push( ptr ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} returned pointer to pool {ptr.ToInt64():X8} (size={size})" ); +#endif + } + } + + private const int Bucket512 = 512; + private const int Bucket1Kb = 1 * 1024; + private const int Bucket4Kb = 4 * 1024; + private const int Bucket16Kb = 16 * 1024; + private const int Bucket64Kb = 64 * 1024; + private const int Bucket256Kb = 256 * 1024; + + private static int GetBucketSize( int size ) + { + if ( size <= Bucket512 ) return Bucket512; + if ( size <= Bucket1Kb ) return Bucket1Kb; + if ( size <= Bucket4Kb ) return Bucket4Kb; + if ( size <= Bucket16Kb ) return Bucket16Kb; + if ( size <= Bucket64Kb ) return Bucket64Kb; + if ( size <= Bucket256Kb ) return Bucket256Kb; + + return -1; + } + + private static int GetBucketLimit( int size ) + { + if ( size <= Bucket512 ) return 1024; + if ( size <= Bucket1Kb ) return 512; + if ( size <= Bucket4Kb ) return 128; + if ( size <= Bucket16Kb ) return 32; + if ( size <= Bucket64Kb ) return 16; + if ( size <= Bucket256Kb ) return 8; + + return -1; + } + } +} diff --git a/Libraries/Facepunch.Steamworks/Networking/Connection.cs b/Libraries/Facepunch.Steamworks/Networking/Connection.cs index 0eb4aafce..d1069a693 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Connection.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Connection.cs @@ -11,13 +11,18 @@ namespace Steamworks.Data /// You can override all the virtual functions to turn it into what you /// want it to do. /// - public struct Connection + public struct Connection : IEquatable { public uint Id { get; set; } + public bool Equals( Connection other ) => Id == other.Id; + public override bool Equals( object obj ) => obj is Connection other && Id == other.Id; + public override int GetHashCode() => Id.GetHashCode(); public override string ToString() => Id.ToString(); public static implicit operator Connection( uint value ) => new Connection() { Id = value }; public static implicit operator uint( Connection value ) => value.Id; + public static bool operator ==( Connection value1, Connection value2 ) => value1.Equals( value2 ); + public static bool operator !=( Connection value1, Connection value2 ) => !value1.Equals( value2 ); /// /// Accept an incoming connection that has been received on a listen socket. @@ -64,21 +69,41 @@ namespace Steamworks.Data /// /// This is the best version to use. /// - public Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { + if ( ptr == IntPtr.Zero ) + throw new ArgumentNullException( nameof( ptr ) ); + if ( size == 0 ) + throw new ArgumentException( "`size` cannot be zero", nameof( size ) ); + + var copyPtr = BufferManager.Get( size, 1 ); + Buffer.MemoryCopy( (void*)ptr, (void*)copyPtr, size, size ); + + var message = SteamNetworkingUtils.AllocateMessage(); + message->Connection = this; + message->Flags = sendType; + message->DataPtr = copyPtr; + message->DataSize = size; + message->FreeDataPtr = BufferManager.FreeFunctionPointer; + message->IdxLane = laneIndex; + long messageNumber = 0; - return SteamNetworkingSockets.Internal?.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ) ?? Result.Fail; + SteamNetworkingSockets.Internal?.SendMessages( 1, &message, &messageNumber ); + + return messageNumber >= 0 + ? Result.OK + : (Result)(-messageNumber); } /// /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and /// you're not creating a new one every frame (like using .ToArray()) /// - public unsafe Result SendMessage( byte[] data, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( byte[] data, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { fixed ( byte* ptr = data ) { - return SendMessage( (IntPtr)ptr, data.Length, sendType ); + return SendMessage( (IntPtr)ptr, data.Length, sendType, laneIndex ); } } @@ -86,21 +111,21 @@ namespace Steamworks.Data /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and /// you're not creating a new one every frame (like using .ToArray()) /// - public unsafe Result SendMessage( byte[] data, int offset, int length, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( byte[] data, int offset, int length, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { fixed ( byte* ptr = data ) { - return SendMessage( (IntPtr)ptr + offset, length, sendType ); + return SendMessage( (IntPtr)ptr + offset, length, sendType, laneIndex ); } } /// /// This creates a ton of garbage - so don't do anything with this beyond testing! /// - public unsafe Result SendMessage( string str, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( string str, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { var bytes = System.Text.Encoding.UTF8.GetBytes( str ); - return SendMessage( bytes, sendType ); + return SendMessage( bytes, sendType, laneIndex ); } /// @@ -121,5 +146,27 @@ namespace Steamworks.Data return strVal; } + + /// + /// Returns a small set of information about the real-time state of the connection. + /// + public ConnectionStatus QuickStatus() + { + ConnectionStatus connectionStatus = default( ConnectionStatus ); + + SteamNetworkingSockets.Internal?.GetConnectionRealTimeStatus( this, ref connectionStatus, 0, null ); + + return connectionStatus; + } + + /// + /// Configure multiple outbound messages streams ("lanes") on a connection, and + /// control head-of-line blocking between them. + /// + public Result ConfigureConnectionLanes( int[] lanePriorities, ushort[] laneWeights ) + { + return SteamNetworkingSockets.Internal?.ConfigureConnectionLanes( this, lanePriorities.Length, lanePriorities, laneWeights ) + ?? Result.Fail; + } } } diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs new file mode 100644 index 000000000..a109542dc --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace Steamworks.Data +{ + /// + /// Describe the status of a connection + /// + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + public struct ConnectionLaneStatus + { + internal int cbPendingUnreliable; // m_cbPendingUnreliable int + internal int cbPendingReliable; // m_cbPendingReliable int + internal int cbSentUnackedReliable; // m_cbSentUnackedReliable int + internal int _reservePad1; // _reservePad1 int + internal long ecQueueTime; // m_usecQueueTime SteamNetworkingMicroseconds + [MarshalAs( UnmanagedType.ByValArray, SizeConst = 10, ArraySubType = UnmanagedType.U4 )] + internal uint[] reserved; // reserved uint32 [10] + + /// + /// Number of bytes unreliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingUnreliable => cbPendingUnreliable; + + /// + /// Number of bytes reliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingReliable => cbPendingReliable; + + /// + /// Number of bytes of reliable data that has been placed the wire, but for which we have not yet received an acknowledgment, and thus we may have to re-transmit. + /// + public int SentUnackedReliable => cbSentUnackedReliable; + } +} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs index 1ac65e609..3116fa804 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs @@ -1,6 +1,5 @@ using Steamworks.Data; using System; -using System.Runtime.InteropServices; namespace Steamworks { @@ -36,7 +35,10 @@ namespace Steamworks set => Connection.UserData = value; } - public void Close() => Connection.Close(); + public void Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" ) + { + Connection.Close( linger, reasonCode, debugString ); + } public override string ToString() => Connection.ToString(); @@ -44,18 +46,40 @@ namespace Steamworks { ConnectionInfo = info; + // + // Some notes: + // - Update state before the callbacks, in case an exception is thrown + // - ConnectionState.None happens when a connection is destroyed, even if it was already disconnected (ClosedByPeer / ProblemDetectedLocally) + // switch ( info.State ) { case ConnectionState.Connecting: - OnConnecting( info ); + if ( !Connecting && !Connected ) + { + Connecting = true; + + OnConnecting( info ); + } break; case ConnectionState.Connected: - OnConnected( info ); + if ( Connecting && !Connected ) + { + Connecting = false; + Connected = true; + + OnConnected( info ); + } break; case ConnectionState.ClosedByPeer: case ConnectionState.ProblemDetectedLocally: case ConnectionState.None: - OnDisconnected( info ); + if ( Connecting || Connected ) + { + Connecting = false; + Connected = false; + + OnDisconnected( info ); + } break; } } @@ -66,8 +90,6 @@ namespace Steamworks public virtual void OnConnecting( ConnectionInfo info ) { Interface?.OnConnecting( info ); - - Connecting = true; } /// @@ -76,9 +98,6 @@ namespace Steamworks public virtual void OnConnected( ConnectionInfo info ) { Interface?.OnConnected( info ); - - Connected = true; - Connecting = false; } /// @@ -87,52 +106,166 @@ namespace Steamworks public virtual void OnDisconnected( ConnectionInfo info ) { Interface?.OnDisconnected( info ); - - Connected = false; - Connecting = false; } - public void Receive( int bufferSize = 32 ) + public unsafe int Receive( int bufferSize = 32, bool receiveToEnd = true ) { - if (SteamNetworkingSockets.Internal is null) { return; } + if (SteamNetworkingSockets.Internal is null) { return 0; } + + if ( bufferSize < 1 || bufferSize > 256 ) throw new ArgumentOutOfRangeException( nameof( bufferSize ) ); + + int totalProcessed = 0; + NetMsg** messageBuffer = stackalloc NetMsg*[bufferSize]; - int processed = 0; - IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); - - try + while ( true ) { - processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, messageBuffer, bufferSize ); + int processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, new IntPtr( &messageBuffer[0] ), bufferSize ); + totalProcessed += processed; - for ( int i = 0; i < processed; i++ ) + try { - ReceiveMessage( Marshal.ReadIntPtr( messageBuffer, i * IntPtr.Size ) ); + for ( int i = 0; i < processed; i++ ) + { + ReceiveMessage( ref messageBuffer[i] ); + } + } + catch + { + for ( int i = 0; i < processed; i++ ) + { + if ( messageBuffer[i] != null ) + { + NetMsg.InternalRelease( messageBuffer[i] ); + } + } + + throw; + } + + + // + // Keep going if receiveToEnd and we filled the buffer + // + if ( !receiveToEnd || processed < bufferSize ) + break; + } + + return totalProcessed; + } + + /// + /// Sends a message to multiple connections. + /// + /// The connections to send the message to. + /// The number of connections to send the message to, to allow reusing the connections array. + /// Pointer to the message data. + /// Size of the message data. + /// Flags to control delivery of the message. + /// An optional array to hold the results of sending the messages for each connection. + public unsafe void SendMessages( Connection[] connections, int connectionCount, IntPtr ptr, int size, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + if ( connections == null ) + throw new ArgumentNullException( nameof( connections ) ); + if ( connectionCount < 0 || connectionCount > connections.Length ) + throw new ArgumentException( "`connectionCount` must be between 0 and `connections.Length`", nameof( connectionCount ) ); + if ( results != null && connectionCount > results.Length ) + throw new ArgumentException( "`results` must have at least `connectionCount` entries", nameof( results ) ); + if ( connectionCount > 1024 ) // restricting this because we stack allocate based on this value + throw new ArgumentOutOfRangeException( nameof( connectionCount ) ); + if ( ptr == IntPtr.Zero ) + throw new ArgumentNullException( nameof( ptr ) ); + if ( size == 0 ) + throw new ArgumentException( "`size` cannot be zero", nameof( size ) ); + + if ( SteamNetworkingSockets.Internal is null ) + return; + if ( connectionCount == 0 ) + return; + + // SendMessages does not make a copy of the data. We will need to copy because we don't want to force the caller to keep the pointer valid. + // 1. We don't want a copy per message. They all refer to the same data. This is the benefit of using Broadcast vs. many sends. + // 2. We need to use unmanaged memory. Managed memory may move around and invalidate pointers so it's not an option. + // 3. We'll use a reference counter and custom free() function to release this unmanaged memory. + var copyPtr = BufferManager.Get( size, connectionCount ); + Buffer.MemoryCopy( (void*)ptr, (void*)copyPtr, size, size ); + + var messages = stackalloc NetMsg*[connectionCount]; + var messageNumberOrResults = stackalloc long[results != null ? connectionCount : 0]; + + for ( var i = 0; i < connectionCount; i++ ) + { + messages[i] = SteamNetworkingUtils.AllocateMessage(); + messages[i]->Connection = connections[i]; + messages[i]->Flags = sendType; + messages[i]->DataPtr = copyPtr; + messages[i]->DataSize = size; + messages[i]->FreeDataPtr = BufferManager.FreeFunctionPointer; + } + + SteamNetworkingSockets.Internal.SendMessages( connectionCount, messages, messageNumberOrResults ); + + if (results == null) + return; + + for ( var i = 0; i < connectionCount; i++ ) + { + if ( messageNumberOrResults[i] < 0 ) + { + results[i] = (Result)( -messageNumberOrResults[i] ); + } + else + { + results[i] = Result.OK; } } - finally - { - Marshal.FreeHGlobal( messageBuffer ); - } - - // - // Overwhelmed our buffer, keep going - // - if ( processed == bufferSize ) - Receive( bufferSize ); } - internal unsafe void ReceiveMessage( IntPtr msgPtr ) + /// + /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and + /// you're not creating a new one every frame (like using .ToArray()) + /// + public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + fixed ( byte* ptr = data ) + { + SendMessages( connections, connectionCount, (IntPtr)ptr, data.Length, sendType, results ); + } + } + + /// + /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and + /// you're not creating a new one every frame (like using .ToArray()) + /// + public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, int offset, int length, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + fixed ( byte* ptr = data ) + { + SendMessages( connections, connectionCount, (IntPtr)ptr + offset, length, sendType, results ); + } + } + + /// + /// This creates a ton of garbage - so don't do anything with this beyond testing! + /// + public void SendMessages( Connection[] connections, int connectionCount, string str, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + var bytes = System.Text.Encoding.UTF8.GetBytes( str ); + SendMessages( connections, connectionCount, bytes, sendType, results ); + } + + internal unsafe void ReceiveMessage( ref NetMsg* msg ) { - var msg = Marshal.PtrToStructure( msgPtr ); try { - OnMessage( msg.DataPtr, msg.DataSize, msg.RecvTime, msg.MessageNumber, msg.Channel ); + OnMessage( msg->DataPtr, msg->DataSize, msg->RecvTime, msg->MessageNumber, msg->Channel ); } finally { // // Releases the message // - NetMsg.InternalRelease( (NetMsg*) msgPtr ); + NetMsg.InternalRelease( msg ); + msg = null; } } @@ -141,4 +274,4 @@ namespace Steamworks Interface?.OnMessage( data, size, messageNum, recvTime, channel ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs new file mode 100644 index 000000000..c217055fc --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs @@ -0,0 +1,77 @@ +using System.Runtime.InteropServices; + +namespace Steamworks.Data +{ + /// + /// Describe the status of a connection + /// + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + public struct ConnectionStatus + { + internal ConnectionState state; // m_eState ESteamNetworkingConnectionState + internal int ping; // m_nPing int + internal float connectionQualityLocal; // m_flConnectionQualityLocal float + internal float connectionQualityRemote; // m_flConnectionQualityRemote float + internal float outPacketsPerSec; // m_flOutPacketsPerSec float + internal float outBytesPerSec; // m_flOutBytesPerSec float + internal float inPacketsPerSec; // m_flInPacketsPerSec float + internal float inBytesPerSec; // m_flInBytesPerSec float + internal int sendRateBytesPerSecond; // m_nSendRateBytesPerSecond int + internal int cbPendingUnreliable; // m_cbPendingUnreliable int + internal int cbPendingReliable; // m_cbPendingReliable int + internal int cbSentUnackedReliable; // m_cbSentUnackedReliable int + internal long ecQueueTime; // m_usecQueueTime SteamNetworkingMicroseconds + [MarshalAs( UnmanagedType.ByValArray, SizeConst = 16, ArraySubType = UnmanagedType.U4 )] + internal uint[] reserved; // reserved uint32 [16] + + /// + /// Current ping (ms) + /// + public int Ping => ping; + + /// + /// Outgoing packets per second + /// + public float OutPacketsPerSec => outPacketsPerSec; + + /// + /// Outgoing bytes per second + /// + public float OutBytesPerSec => outBytesPerSec; + + /// + /// Incoming packets per second + /// + public float InPacketsPerSec => inPacketsPerSec; + + /// + /// Incoming bytes per second + /// + public float InBytesPerSec => inBytesPerSec; + + /// + /// Connection quality measured locally, 0...1 (percentage of packets delivered end-to-end in order). + /// + public float ConnectionQualityLocal => connectionQualityLocal; + + /// + /// Packet delivery success rate as observed from remote host, 0...1 (percentage of packets delivered end-to-end in order). + /// + public float ConnectionQualityRemote => connectionQualityRemote; + + /// + /// Number of bytes unreliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingUnreliable => cbPendingUnreliable; + + /// + /// Number of bytes reliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingReliable => cbPendingReliable; + + /// + /// Number of bytes of reliable data that has been placed the wire, but for which we have not yet received an acknowledgment, and thus we may have to re-transmit. + /// + public int SentUnackedReliable => cbSentUnackedReliable; + } +} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/Delegates.cs b/Libraries/Facepunch.Steamworks/Networking/Delegates.cs new file mode 100644 index 000000000..2ab13e14e --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/Delegates.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Steamworks.Data; + +namespace Steamworks +{ + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void NetDebugFunc( [In] NetDebugOutput nType, [In] IntPtr pszMsg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal unsafe delegate void FnSteamNetConnectionStatusChanged( ref SteamNetConnectionStatusChangedCallback_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetAuthenticationStatusChanged( ref SteamNetAuthenticationStatus_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamRelayNetworkStatusChanged( ref SteamRelayNetworkStatus_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetworkingMessagesSessionRequest( ref SteamNetworkingMessagesSessionRequest_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetworkingMessagesSessionFailed( ref SteamNetworkingMessagesSessionFailed_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetworkingFakeIPResult( ref SteamNetworkingFakeIPResult_t arg ); +} diff --git a/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs index faab31da9..44d67970b 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs @@ -16,7 +16,7 @@ namespace Steamworks void OnConnected( Connection connection, ConnectionInfo info ); /// - /// Called when the connection leaves + /// Called when the connection leaves. Must call Close on the connection /// void OnDisconnected( Connection connection, ConnectionInfo info ); diff --git a/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs b/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs index 09db1e883..3338382e2 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs @@ -110,6 +110,18 @@ namespace Steamworks.Data } } + /// + /// Return true if IP is a fake IPv4 for Steam Datagram Relay + /// + public bool IsFakeIPv4 + { + get + { + NetAddress self = this; + return SteamNetworkingUtils.Internal != null && SteamNetworkingUtils.Internal.IsFakeIPv4( InternalGetIPv4( ref self ) ); + } + } + /// /// Return true if this identity is localhost. (Either IPv6 ::1, or IPv4 127.0.0.1) /// diff --git a/Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs b/Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs deleted file mode 100644 index ec4cabdec..000000000 --- a/Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Steamworks.Data; -using System; -using System.Runtime.InteropServices; - -namespace Steamworks.Data -{ - [UnmanagedFunctionPointer( Platform.CC )] - delegate void NetDebugFunc( [In] NetDebugOutput nType, [In] IntPtr pszMsg ); -} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs b/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs index 88bc7ee2c..5ce0661a9 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; namespace Steamworks.Data { [StructLayout( LayoutKind.Explicit, Pack = Platform.StructPlatformPackSize )] - internal struct NetKeyValue + internal partial struct NetKeyValue { [FieldOffset(0)] internal NetConfig Value; // m_eValue ESteamNetworkingConfigValue diff --git a/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs b/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs index 8bd095384..63091cffc 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs @@ -1,5 +1,4 @@ -using Steamworks.Data; -using System; +using System; using System.Runtime.InteropServices; namespace Steamworks.Data @@ -17,5 +16,9 @@ namespace Steamworks.Data internal IntPtr FreeDataPtr; internal IntPtr ReleasePtr; internal int Channel; + internal SendType Flags; + internal long UserData; + internal ushort IdxLane; + internal ushort _pad1__; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs index 1d9721cf4..4e36a0926 100644 --- a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs @@ -16,8 +16,9 @@ namespace Steamworks { public ISocketManager? Interface { get; set; } - public List Connecting = new List(); - public List Connected = new List(); + public HashSet Connecting = new HashSet(); + public HashSet Connected = new HashSet(); + public Socket Socket { get; internal set; } public override string ToString() => Socket.ToString(); @@ -44,17 +45,23 @@ namespace Steamworks public virtual void OnConnectionChanged( Connection connection, ConnectionInfo info ) { + // + // Some notes: + // - Update state before the callbacks, in case an exception is thrown + // - ConnectionState.None happens when a connection is destroyed, even if it was already disconnected (ClosedByPeer / ProblemDetectedLocally) + // switch ( info.State ) { case ConnectionState.Connecting: - if ( !Connecting.Contains( connection ) ) + if ( !Connecting.Contains( connection ) && !Connected.Contains( connection ) ) { Connecting.Add( connection ); + OnConnecting( connection, info ); } break; case ConnectionState.Connected: - if ( !Connected.Contains( connection ) ) + if ( Connecting.Contains( connection ) && !Connected.Contains( connection ) ) { Connecting.Remove( connection ); Connected.Add( connection ); @@ -67,6 +74,9 @@ namespace Steamworks case ConnectionState.None: if ( Connecting.Contains( connection ) || Connected.Contains( connection ) ) { + Connecting.Remove( connection ); + Connected.Remove( connection ); + OnDisconnected( connection, info ); } break; @@ -81,7 +91,6 @@ namespace Steamworks if ( Interface != null ) { Interface.OnConnecting( connection, info ); - return; } else { @@ -104,17 +113,17 @@ namespace Steamworks /// public virtual void OnDisconnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, 0 ); - - connection.Close(); - - Connecting.Remove( connection ); - Connected.Remove( connection ); - - Interface?.OnDisconnected( connection, info ); + if ( Interface != null ) + { + Interface.OnDisconnected( connection, info ); + } + else + { + connection.Close(); + } } - public void Receive( int bufferSize = 32 ) + public int Receive( int bufferSize = 32, bool receiveToEnd = true ) { int processed = 0; IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); @@ -137,8 +146,10 @@ namespace Steamworks // // Overwhelmed our buffer, keep going // - if ( processed == bufferSize ) - Receive( bufferSize ); + if ( receiveToEnd && processed == bufferSize ) + processed += Receive( bufferSize ); + + return processed; } internal unsafe void ReceiveMessage( IntPtr msgPtr ) @@ -162,4 +173,4 @@ namespace Steamworks Interface?.OnMessage( connection, identity, data, size, messageNum, recvTime, channel ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamApps.cs b/Libraries/Facepunch.Steamworks/SteamApps.cs index 80af70a5e..80359e884 100644 --- a/Libraries/Facepunch.Steamworks/SteamApps.cs +++ b/Libraries/Facepunch.Steamworks/SteamApps.cs @@ -15,9 +15,12 @@ namespace Steamworks { internal static ISteamApps? Internal => Interface as ISteamApps; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamApps( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } internal static void InstallEvents() @@ -27,41 +30,41 @@ namespace Steamworks } /// - /// posted after the user gains ownership of DLC and that DLC is installed + /// Posted after the user gains ownership of DLC and that DLC is installed. /// public static event Action? OnDlcInstalled; /// - /// posted after the user gains executes a Steam URL with command line or query parameters + /// Posted after the user gains executes a Steam URL with command line or query parameters /// such as steam://run/appid//-commandline/?param1=value1(and)param2=value2(and)param3=value3 etc /// while the game is already running. The new params can be queried - /// with GetLaunchQueryParam and GetLaunchCommandLine + /// with GetLaunchQueryParam and GetLaunchCommandLine. /// public static event Action? OnNewLaunchParameters; /// - /// Checks if the active user is subscribed to the current App ID + /// Gets whether or not the active user is subscribed to the current App ID. /// public static bool IsSubscribed => Internal != null && Internal.BIsSubscribed(); /// - /// Check if user borrowed this game via Family Sharing, If true, call GetAppOwner() to get the lender SteamID + /// Gets whether or not the user borrowed this game via Family Sharing. If true, call GetAppOwner() to get the lender SteamID. /// public static bool IsSubscribedFromFamilySharing => Internal != null && Internal.BIsSubscribedFromFamilySharing(); /// - /// Checks if the license owned by the user provides low violence depots. + /// Gets whether or not the license owned by the user provides low violence depots. /// Low violence depots are useful for copies sold in countries that have content restrictions /// public static bool IsLowViolence => Internal != null && Internal.BIsLowViolence(); /// - /// Checks whether the current App ID license is for Cyber Cafes. + /// Gets whether or not the current App ID license is for Cyber Cafes. /// public static bool IsCybercafe => Internal != null && Internal.BIsCybercafe(); /// - /// CChecks if the user has a VAC ban on their account + /// Gets whether or not the user has a VAC ban on their account. /// public static bool IsVACBanned => Internal != null && Internal.BIsVACBanned(); @@ -77,19 +80,22 @@ namespace Steamworks public static string[]? AvailableLanguages => Internal?.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); /// - /// Checks if the active user is subscribed to a specified AppId. + /// Gets whether or not the active user is subscribed to a specified App ID. /// Only use this if you need to check ownership of another game related to yours, a demo for example. /// + /// The App ID of the DLC to check. public static bool IsSubscribedToApp( AppId appid ) => Internal != null && Internal.BIsSubscribedApp( appid.Value ); /// - /// Checks if the user owns a specific DLC and if the DLC is installed + /// Gets whether or not the user owns a specific DLC and if the DLC is installed. /// + /// The App ID of the DLC to check. public static bool IsDlcInstalled( AppId appid ) => Internal != null && Internal.BIsDlcInstalled( appid.Value ); /// - /// Returns the time of the purchase of the app + /// Returns the time of the purchase of the app. /// + /// The App ID to check the purchase time for. public static DateTime PurchaseTime( AppId appid = default ) { if (Internal is null) { return default; } @@ -101,14 +107,14 @@ namespace Steamworks } /// - /// Checks if the user is subscribed to the current app through a free weekend - /// This function will return false for users who have a retail or other type of license - /// Before using, please ask your Valve technical contact how to package and secure your free weekened + /// Checks if the user is subscribed to the current app through a free weekend. + /// This function will return false for users who have a retail or other type of license. + /// Before using, please ask your Valve technical contact how to package and secure your free weekened. /// public static bool IsSubscribedFromFreeWeekend => Internal != null && Internal.BIsSubscribedFromFreeWeekend(); /// - /// Returns metadata for all available DLC + /// Returns metadata for all available DLC. /// public static IEnumerable DlcInformation() { @@ -134,17 +140,19 @@ namespace Steamworks } /// - /// Install/Uninstall control for optional DLC + /// Install control for optional DLC. /// + /// The App ID of the DLC to install. public static void InstallDlc( AppId appid ) => Internal?.InstallDLC( appid.Value ); /// - /// Install/Uninstall control for optional DLC + /// Uninstall control for optional DLC. /// + /// The App ID of the DLC to uninstall. public static void UninstallDlc( AppId appid ) => Internal?.UninstallDLC( appid.Value ); /// - /// Returns null if we're not on a beta branch, else the name of the branch + /// Gets the name of the beta branch that is launched, or if the application is not running on a beta branch. /// public static string? CurrentBetaName { @@ -158,16 +166,19 @@ namespace Steamworks } /// - /// Allows you to force verify game content on next launch. - /// - /// If you detect the game is out-of-date(for example, by having the client detect a version mismatch with a server), - /// you can call use MarkContentCorrupt to force a verify, show a message to the user, and then quit. + /// Force verify game content on next launch. + /// + /// If you detect the game is out-of-date (for example, by having the client detect a version mismatch with a server), + /// you can call MarkContentCorrupt to force a verify, show a message to the user, and then quit. + /// /// + /// Whether or not to only verify missing files. public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal?.MarkContentCorrupt( missingFilesOnly ); /// - /// Gets a list of all installed depots for a given App ID in mount order + /// Gets a list of all installed depots for a given App ID in mount order. /// + /// The App ID. public static IEnumerable InstalledDepots( AppId appid = default ) { if (Internal is null) { yield break; } @@ -185,9 +196,10 @@ namespace Steamworks } /// - /// Gets the install folder for a specific AppID. + /// Gets the install folder for a specific App ID. /// This works even if the application is not installed, based on where the game would be installed with the default Steam library location. /// + /// The App ID. public static string? AppInstallDir( AppId appid = default ) { if ( appid == 0 ) @@ -200,26 +212,32 @@ namespace Steamworks } /// - /// The app may not actually be owned by the current user, they may have it left over from a free weekend, etc. + /// Gets whether or not the app is owned by the current user. The app may not actually be owned by the current user; they may have it left over from a free weekend, etc. /// + /// The App ID. public static bool IsAppInstalled( AppId appid ) => Internal != null && Internal.BIsAppInstalled( appid.Value ); /// - /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed.. + /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed. /// public static SteamId AppOwner => Internal?.GetAppOwner().Value ?? default; /// /// Gets the associated launch parameter if the game is run via steam://run/appid/?param1=value1;param2=value2;param3=value3 etc. - /// Parameter names starting with the character '@' are reserved for internal use and will always return an empty string. - /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, + /// + /// Parameter names starting with the character '@' are reserved for internal use and will always return an empty string. + /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, /// but it is advised that you not param names beginning with an underscore for your own features. + /// /// + /// The name of the parameter. + /// The launch parameter value. public static string? GetLaunchParam( string param ) => Internal?.GetLaunchQueryParam( param ); /// /// Gets the download progress for optional DLC. /// + /// The App ID to check the progress for. public static DownloadProgress DlcDownloadProgress( AppId appid ) { ulong punBytesDownloaded = 0; @@ -232,16 +250,16 @@ namespace Steamworks } /// - /// Gets the buildid of this app, may change at any time based on backend updates to the game. - /// Defaults to 0 if you're not running a build downloaded from steam. + /// Gets the Build ID of this app, which can change at any time based on backend updates to the game. + /// Defaults to 0 if you're not running a build downloaded from steam. /// public static int BuildId => Internal?.GetAppBuildId() ?? 0; /// /// Asynchronously retrieves metadata details about a specific file in the depot manifest. - /// Currently provides: /// + /// The name of the file. public static async Task GetFileDetailsAsync( string filename ) { if (Internal is null) { return null; } @@ -262,9 +280,9 @@ namespace Steamworks /// Get command line if game was launched via Steam URL, e.g. steam://run/appid//command line/. /// This method of passing a connect string (used when joining via rich presence, accepting an /// invite, etc) is preferable to passing the connect string on the operating system command - /// line, which is a security risk. In order for rich presence joins to go through this + /// line, which is a security risk. In order for rich presence joins to go through this /// path and not be placed on the OS command line, you must set a value in your app's - /// configuration on Steam. Ask Valve for help with this. + /// configuration on Steam. Ask Valve for help with this. /// public static string? CommandLine { @@ -276,5 +294,26 @@ namespace Steamworks } } + /// + /// Check if game is a timed trial with limited playtime. + /// + /// The amount of seconds left on the timed trial. + /// The amount of seconds played on the timed trial. + public static bool IsTimedTrial( out int secondsAllowed, out int secondsPlayed ) + { + uint a = 0; + uint b = 0; + secondsAllowed = 0; + secondsPlayed = 0; + + if ( Internal == null || !Internal.BIsTimedTrial( ref a, ref b ) ) + return false; + + secondsAllowed = (int) a; + secondsPlayed = (int) b; + + return true; + } + } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamClient.cs b/Libraries/Facepunch.Steamworks/SteamClient.cs index 31ef55045..f0db1417c 100644 --- a/Libraries/Facepunch.Steamworks/SteamClient.cs +++ b/Libraries/Facepunch.Steamworks/SteamClient.cs @@ -13,7 +13,7 @@ namespace Steamworks /// /// Initialize the steam client. - /// If asyncCallbacks is false you need to call RunCallbacks manually every frame. + /// If is false you need to call manually every frame. /// public static void Init( uint appid, bool asyncCallbacks = true ) { @@ -25,7 +25,7 @@ namespace Steamworks if ( !SteamAPI.Init() ) { - throw new System.Exception( "SteamApi_Init returned false. Steam isn't running, couldn't find Steam, AppId is ureleased, Don't own AppId." ); + throw new System.Exception( "SteamApi_Init returned false. Steam isn't running, couldn't find Steam, App ID is ureleased, Don't own App ID." ); } AppId = appid; @@ -60,7 +60,9 @@ namespace Steamworks AddInterface(); AddInterface(); - if ( asyncCallbacks ) + initialized = openInterfaces.Count > 0; + + if ( asyncCallbacks ) { // // This will keep looping in the background every 16 ms @@ -73,8 +75,15 @@ namespace Steamworks internal static void AddInterface() where T : SteamClass, new() { var t = new T(); - t.InitializeInterface( false ); - openInterfaces.Add( t ); + bool valid = t.InitializeInterface( false ); + if ( valid ) + { + openInterfaces.Add( t ); + } + else + { + t.DestroyInterface( false ); + } } static readonly List openInterfaces = new List(); @@ -91,6 +100,9 @@ namespace Steamworks public static bool IsValid => initialized; + /// + /// Shuts down the steam client. + /// public static void Shutdown() { if ( !IsValid ) return; @@ -116,13 +128,15 @@ namespace Steamworks /// /// Checks if the current user's Steam client is connected to the Steam servers. - /// If it's not then no real-time services provided by the Steamworks API will be enabled. The Steam + /// + /// If it's not, no real-time services provided by the Steamworks API will be enabled. The Steam /// client will automatically be trying to recreate the connection as often as possible. When the /// connection is restored a SteamServersConnected_t callback will be posted. /// You usually don't need to check for this yourself. All of the API calls that rely on this will /// check internally. Forcefully disabling stuff when the player loses access is usually not a /// very good experience for the player and you could be preventing them from accessing APIs that do not /// need a live connection to Steam. + /// /// public static bool IsLoggedOn => SteamUser.Internal != null && SteamUser.Internal.BLoggedOn(); @@ -135,28 +149,30 @@ namespace Steamworks public static SteamId SteamId => SteamUser.Internal?.GetSteamID() ?? default; /// - /// returns the local players name - guaranteed to not be NULL. - /// this is the same name as on the users community profile page + /// returns the local players name - guaranteed to not be . + /// This is the same name as on the user's community profile page. /// public static string? Name => SteamFriends.Internal?.GetPersonaName(); /// - /// gets the status of the current user + /// Gets the status of the current user. /// public static FriendState State => SteamFriends.Internal?.GetPersonaState() ?? FriendState.Offline; /// - /// returns the appID of the current process + /// Returns the App ID of the current process. /// public static AppId AppId { get; internal set; } /// - /// Checks if your executable was launched through Steam and relaunches it through Steam if it wasn't - /// this returns true then it starts the Steam client if required and launches your game again through it, + /// Checks if your executable was launched through Steam and relaunches it through Steam if it wasn't. + /// + /// This returns true then it starts the Steam client if required and launches your game again through it, /// and you should quit your process as soon as possible. This effectively runs steam://run/AppId so it /// may not relaunch the exact executable that called it, as it will always relaunch from the version /// installed in your Steam library folder/ /// Note that during development, when not launching via Steam, this might always return true. + /// /// public static bool RestartAppIfNecessary( uint appid ) { @@ -178,4 +194,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamFriends.cs b/Libraries/Facepunch.Steamworks/SteamFriends.cs index bcc457525..b251db233 100644 --- a/Libraries/Facepunch.Steamworks/SteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/SteamFriends.cs @@ -8,19 +8,22 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Friends API. /// public class SteamFriends : SteamClientClass { internal static ISteamFriends? Internal => Interface as ISteamFriends; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamFriends( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; richPresence = new Dictionary(); InstallEvents(); + + return true; } static Dictionary? richPresence; @@ -30,53 +33,67 @@ namespace Steamworks Dispatch.Install( x => OnPersonaStateChange?.Invoke( new Friend( x.SteamID ) ) ); Dispatch.Install( x => OnGameRichPresenceJoinRequested?.Invoke( new Friend( x.SteamIDFriend), x.ConnectUTF8() ) ); Dispatch.Install( OnFriendChatMessage ); + Dispatch.Install( OnGameConnectedClanChatMessage ); Dispatch.Install( x => OnGameOverlayActivated?.Invoke( x.Active != 0 ) ); Dispatch.Install( x => OnGameServerChangeRequested?.Invoke( x.ServerUTF8(), x.PasswordUTF8() ) ); Dispatch.Install( x => OnGameLobbyJoinRequested?.Invoke( new Lobby( x.SteamIDLobby ), x.SteamIDFriend ) ); Dispatch.Install( x => OnFriendRichPresenceUpdate?.Invoke( new Friend( x.SteamIDFriend ) ) ); + Dispatch.Install( x => OnOverlayBrowserProtocol?.Invoke( x.RgchURIUTF8() ) ); } /// - /// Called when chat message has been received from a friend. You'll need to turn on - /// ListenForFriendsMessages to recieve this. (friend, msgtype, message) + /// Invoked when a chat message has been received from a friend. You'll need to enable + /// to recieve this. (friend, msgtype, message) /// public static event Action? OnChatMessage; /// - /// called when a friends' status changes + /// Invoked when a chat message has been received in a Steam group chat that we are in. Associated Functions: JoinClanChatRoom. (friend, msgtype, message) + /// + public static event Action? OnClanChatMessage; + + /// + /// Invoked when a friends' status changes. /// public static event Action? OnPersonaStateChange; /// - /// Called when the user tries to join a game from their friends list - /// rich presence will have been set with the "connect" key which is set here + /// Invoked when the user tries to join a game from their friends list. + /// Rich presence will have been set with the "connect" key which is set here. /// public static event Action? OnGameRichPresenceJoinRequested; /// - /// Posted when game overlay activates or deactivates - /// the game can use this to be pause or resume single player games + /// Invoked when game overlay activates or deactivates. + /// The game can use this to be pause or resume single player games. /// public static event Action? OnGameOverlayActivated; /// - /// Called when the user tries to join a different game server from their friends list - /// game client should attempt to connect to specified server when this is received + /// Invoked when the user tries to join a different game server from their friends list. + /// Game client should attempt to connect to specified server when this is received. /// public static event Action? OnGameServerChangeRequested; /// - /// Called when the user tries to join a lobby from their friends list - /// game client should attempt to connect to specified lobby when this is received + /// Invoked when the user tries to join a lobby from their friends list. + /// Game client should attempt to connect to specified lobby when this is received. /// public static event Action? OnGameLobbyJoinRequested; /// - /// Callback indicating updated data about friends rich presence information + /// Invoked when a friend's rich presence data is updated. /// public static event Action? OnFriendRichPresenceUpdate; + /// + /// Invoked when an overlay browser instance is navigated to a + /// protocol/scheme registered by . + /// + public static event Action? OnOverlayBrowserProtocol; + + static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data ) { if ( OnChatMessage == null ) return; @@ -96,7 +113,29 @@ namespace Steamworks OnChatMessage( friend, typeName, message ); } - + + static unsafe void OnGameConnectedClanChatMessage( GameConnectedClanChatMsg_t data ) + { + if ( OnClanChatMessage == null ) return; + if ( Internal is null ) return; + + var friend = new Friend( data.SteamIDUser ); + + using var buffer = Helpers.TakeMemory(); + var type = ChatEntryType.ChatMsg; + SteamId chatter = data.SteamIDUser; + + var len = Internal.GetClanChatMessage( data.SteamIDClanChat, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type, ref chatter ); + + if ( len == 0 && type == ChatEntryType.Invalid ) + return; + + var typeName = type.ToString(); + var message = Helpers.MemoryToString( buffer ); + + OnClanChatMessage( friend, typeName, message ); + } + private static IEnumerable GetFriendsWithFlag(FriendFlags flag) { if (Internal is null) { yield break; } @@ -108,21 +147,28 @@ namespace Steamworks } } - public static string? GetFriendPersonaName( SteamId steamId ) - { - return Internal?.GetFriendPersonaName(steamId); - } - + /// + /// Gets an of friends that the current user has. + /// + /// An of friends. public static IEnumerable GetFriends() { return GetFriendsWithFlag(FriendFlags.Immediate); } + /// + /// Gets an of blocked users that the current user has. + /// + /// An of blocked users. public static IEnumerable GetBlocked() { return GetFriendsWithFlag(FriendFlags.Blocked); } + /// + /// Gets an of friend requests that the current user has. + /// + /// An of friend requests. public static IEnumerable GetFriendsRequested() { return GetFriendsWithFlag( FriendFlags.FriendshipRequested ); @@ -177,7 +223,7 @@ namespace Steamworks } /// - /// The dialog to open. Valid options are: + /// Opens a specific overlay window. Valid options are: /// "friends", /// "community", /// "players", @@ -204,7 +250,7 @@ namespace Steamworks /// /// Activates the Steam Overlay to the Steam store page for the provided app. /// - public static void OpenStoreOverlay( AppId id ) => Internal?.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); + public static void OpenStoreOverlay( AppId id, OverlayToStoreFlag overlayToStoreFlag = OverlayToStoreFlag.None ) => Internal?.ActivateGameOverlayToStore( id.Value, overlayToStoreFlag ); /// /// Activates Steam Overlay web browser directly to the specified URL. @@ -249,6 +295,11 @@ namespace Steamworks await Task.Delay( 500 ); } + /// + /// Returns a small avatar of the user with the given . + /// + /// The of the user to get. + /// A with a value if the image was successfully retrieved. public static async Task GetSmallAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } @@ -256,6 +307,11 @@ namespace Steamworks return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) ); } + /// + /// Returns a medium avatar of the user with the given . + /// + /// The of the user to get. + /// A with a value if the image was successfully retrieved. public static async Task GetMediumAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } @@ -263,6 +319,11 @@ namespace Steamworks return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) ); } + /// + /// Returns a large avatar of the user with the given . + /// + /// The of the user to get. + /// A with a value if the image was successfully retrieved. public static async Task GetLargeAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } @@ -335,6 +396,11 @@ namespace Steamworks } } + /// + /// Gets whether or not the current user is following the user with the given . + /// + /// The to check. + /// Boolean. public static async Task IsFollowing(SteamId steamID) { if (Internal is null) { return false; } @@ -369,5 +435,29 @@ namespace Steamworks return steamIds.ToArray(); } - } + + /// + /// Call this before calling ActivateGameOverlayToWebPage() to have the Steam Overlay Browser block navigations + /// to your specified protocol (scheme) uris and instead dispatch a OverlayBrowserProtocolNavigation callback to your game. + /// + public static bool RegisterProtocolInOverlayBrowser( string protocol ) + { + return Internal != null && Internal.RegisterProtocolInOverlayBrowser( protocol ); + } + + public static async Task JoinClanChatRoom( SteamId chatId ) + { + if ( Internal is null ) return false; + var result = await Internal.JoinClanChatRoom( chatId ); + if ( !result.HasValue ) + return false; + + return result.Value.ChatRoomEnterResponse == RoomEnter.Success ; + } + + public static bool SendClanChatRoomMessage( SteamId chatId, string message ) + { + return Internal != null && Internal.SendClanChatMessage( chatId, message ); + } + } } diff --git a/Libraries/Facepunch.Steamworks/SteamInput.cs b/Libraries/Facepunch.Steamworks/SteamInput.cs index 3db2293b4..bd3c477ad 100644 --- a/Libraries/Facepunch.Steamworks/SteamInput.cs +++ b/Libraries/Facepunch.Steamworks/SteamInput.cs @@ -1,34 +1,41 @@ -using Steamworks.Data; +using System; +using Steamworks.Data; using System.Collections.Generic; namespace Steamworks { + /// + /// Class for utilizing Steam Input. + /// public class SteamInput : SteamClientClass { internal static ISteamInput? Internal => Interface as ISteamInput; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamInput( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } internal const int STEAM_CONTROLLER_MAX_COUNT = 16; /// - /// You shouldn't really need to call this because it get called by RunCallbacks on SteamClient + /// You shouldn't really need to call this because it gets called by /// but Valve think it might be a nice idea if you call it right before you get input info - /// just to make sure the info you're getting is 100% up to date. /// public static void RunFrame() { - Internal?.RunFrame(); + Internal?.RunFrame( false ); } static readonly InputHandle_t[] queryArray = new InputHandle_t[STEAM_CONTROLLER_MAX_COUNT]; /// - /// Return a list of connected controllers. + /// Gets a list of connected controllers. /// public static IEnumerable Controllers { @@ -65,10 +72,43 @@ namespace Steamworks ref origin ); - return Internal.GetGlyphForActionOrigin(origin); + return Internal.GetGlyphForActionOrigin_Legacy(origin); } - internal static Dictionary DigitalHandles = new Dictionary(); + + /// + /// Return an absolute path to the PNG image glyph for the provided digital action name. The current + /// action set in use for the controller will be used for the lookup. You should cache the result and + /// maintain your own list of loaded PNG assets. + /// + public static string? GetPngActionGlyph( Controller controller, string action, GlyphSize size ) + { + if ( Internal is null ) { return null; } + + InputActionOrigin origin = InputActionOrigin.None; + + Internal.GetDigitalActionOrigins( controller.Handle, Internal.GetCurrentActionSet( controller.Handle ), GetDigitalActionHandle( action ), ref origin ); + + return Internal.GetGlyphPNGForActionOrigin( origin, size, 0 ); + } + + /// + /// Return an absolute path to the SVF image glyph for the provided digital action name. The current + /// action set in use for the controller will be used for the lookup. You should cache the result and + /// maintain your own list of loaded PNG assets. + /// + public static string? GetSvgActionGlyph( Controller controller, string action ) + { + if ( Internal is null ) { return null; } + + InputActionOrigin origin = InputActionOrigin.None; + + Internal.GetDigitalActionOrigins( controller.Handle, Internal.GetCurrentActionSet( controller.Handle ), GetDigitalActionHandle( action ), ref origin ); + + return Internal.GetGlyphSVGForActionOrigin( origin, 0 ); + } + + internal static Dictionary DigitalHandles = new Dictionary(); internal static InputDigitalActionHandle_t GetDigitalActionHandle( string name ) { if (Internal is null) { return default; } @@ -107,4 +147,4 @@ namespace Steamworks return val; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamInventory.cs b/Libraries/Facepunch.Steamworks/SteamInventory.cs index f45131dd1..e8043deca 100644 --- a/Libraries/Facepunch.Steamworks/SteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/SteamInventory.cs @@ -10,17 +10,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Inventory API. /// public class SteamInventory : SteamSharedClass { internal static ISteamInventory? Internal => Interface as ISteamInventory; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamInventory( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -65,7 +68,7 @@ namespace Steamworks /// /// Call this if you're going to want to access definition information. You should be able to get /// away with calling this once at the start if your game, assuming your items don't change all the time. - /// This will trigger OnDefinitionsUpdated at which point Definitions should be set. + /// This will trigger at which point Definitions should be set. /// public static void LoadItemDefinitions() { @@ -83,7 +86,7 @@ namespace Steamworks } /// - /// Will call LoadItemDefinitions and wait until Definitions is not null + /// Will call and wait until Definitions is not null /// public static async Task WaitForDefinitions( float timeoutSeconds = 30 ) { @@ -302,7 +305,7 @@ namespace Steamworks /// - /// Grant all promotional items the user is eligible for + /// Grant all promotional items the user is eligible for. /// public static async Task GrantPromoItemsAsync() { @@ -351,8 +354,9 @@ namespace Steamworks { if (Internal is null) { return null; } - var item_i = items.Select( x => x._id ).ToArray(); - var item_q = items.Select( x => (uint)1 ).ToArray(); + var d = items.GroupBy( x => x._id ).ToDictionary( x => x.Key, x => (uint) x.Count() ); + var item_i = d.Keys.ToArray(); + var item_q = d.Values.ToArray(); var r = await Internal.StartPurchase( item_i, item_q, (uint)item_i.Length ); if ( !r.HasValue ) return null; @@ -366,4 +370,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index df3ac42eb..d8bc55153 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -8,17 +8,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Functions for clients to access matchmaking services, favorites, and to operate on game lobbies + /// Methods for clients to access matchmaking services, favorites, and to operate on game lobbies /// public class SteamMatchmaking : SteamClientClass { internal static ISteamMatchmaking? Internal => Interface as ISteamMatchmaking; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamMatchmaking( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents(); + + return true; } /// @@ -101,69 +104,69 @@ namespace Steamworks } /// - /// Someone invited you to a lobby + /// Invoked when the current user is invited to a lobby. /// public static event Action? OnLobbyInvite; /// - /// You joined a lobby + /// Invoked when the current user joins a lobby. /// public static event Action? OnLobbyEntered; /// - /// You created a lobby + /// Invoked when the current user creates a lobby. /// public static event Action? OnLobbyCreated; /// - /// A game server has been associated with the lobby + /// Invoked when a game server has been associated with a lobby. /// public static event Action? OnLobbyGameCreated; /// - /// The lobby metadata has changed + /// Invoked when a lobby's metadata is modified. /// public static event Action? OnLobbyDataChanged; /// - /// The lobby member metadata has changed + /// Invoked when a member in a lobby's metadata is modified. /// public static event Action? OnLobbyMemberDataChanged; /// - /// The lobby member joined + /// Invoked when a member joins a lobby. /// public static event Action? OnLobbyMemberJoined; /// - /// The lobby member left the room + /// Invoked when a lobby member leaves the lobby. /// public static event Action? OnLobbyMemberLeave; /// - /// The lobby member left the room + /// Invoked when a lobby member leaves the lobby. /// public static event Action? OnLobbyMemberDisconnected; /// - /// The lobby member was kicked. The 3rd param is the user that kicked them. + /// Invoked when a lobby member is kicked from a lobby. The 3rd param is the user that kicked them. /// public static event Action? OnLobbyMemberKicked; /// - /// The lobby member was banned. The 3rd param is the user that banned them. + /// Invoked when a lobby member is kicked from a lobby. The 3rd param is the user that kicked them. /// public static event Action? OnLobbyMemberBanned; /// - /// A chat message was recieved from a member of a lobby + /// Invoked when a chat message is received from a member of the lobby. /// public static event Action? OnChatMessage; public static LobbyQuery CreateLobbyQuery() { return new LobbyQuery(); } /// - /// Creates a new invisible lobby. Call lobby.SetPublic to take it online. + /// Creates a new invisible lobby. Call to take it online. /// public static async Task CreateLobbyAsync( int maxMembers = 100 ) { @@ -176,7 +179,7 @@ namespace Steamworks } /// - /// Attempts to directly join the specified lobby + /// Attempts to directly join the specified lobby. /// public static async Task JoinLobbyAsync( SteamId lobbyId ) { @@ -189,7 +192,7 @@ namespace Steamworks } /// - /// Get a list of servers that are on your favorites list + /// Get a list of servers that are on the current user's favorites list. /// public static IEnumerable GetFavoriteServers() { @@ -215,7 +218,7 @@ namespace Steamworks } /// - /// Get a list of servers that you have added to your play history + /// Get a list of servers that the current user has added to their history. /// public static IEnumerable GetHistoryServers() { @@ -241,4 +244,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs index a1d7d2c71..f44c9cd01 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs @@ -8,15 +8,18 @@ using Steamworks.Data; namespace Steamworks { /// - /// Functions for clients to access matchmaking services, favorites, and to operate on game lobbies + /// Methods for clients to access matchmaking services, favorites, and to operate on game lobbies /// internal class SteamMatchmakingServers : SteamClientClass { internal static ISteamMatchmakingServers? Internal => Interface as ISteamMatchmakingServers; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamMatchmakingServers( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamMusic.cs b/Libraries/Facepunch.Steamworks/SteamMusic.cs index b0d6ef42d..7578aa876 100644 --- a/Libraries/Facepunch.Steamworks/SteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/SteamMusic.cs @@ -17,11 +17,13 @@ namespace Steamworks { internal static ISteamMusic? Internal => Interface as ISteamMusic; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamMusic( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents(); + return true; } internal static void InstallEvents() @@ -31,22 +33,22 @@ namespace Steamworks } /// - /// Playback status changed + /// Invoked when playback status is changed. /// public static event Action? OnPlaybackChanged; /// - /// Volume changed, parameter is new volume + /// Invoked when the volume of the music player is changed. /// public static event Action? OnVolumeChanged; /// - /// Checks if Steam Music is enabled + /// Checks if Steam Music is enabled. /// public static bool IsEnabled => Internal != null && Internal.BIsEnabled(); /// - /// true if a song is currently playing, paused, or queued up to play; otherwise false. + /// if a song is currently playing, paused, or queued up to play; otherwise . /// public static bool IsPlaying => Internal != null && Internal.BIsPlaying(); @@ -55,23 +57,28 @@ namespace Steamworks /// public static MusicStatus Status => Internal?.GetPlaybackStatus() ?? MusicStatus.Undefined; - + /// + /// Plays the music player. + /// public static void Play() => Internal?.Play(); + /// + /// Pauses the music player. + /// public static void Pause() => Internal?.Pause(); /// - /// Have the Steam Music player play the previous song. + /// Forces the music player to play the previous song. /// public static void PlayPrevious() => Internal?.PlayPrevious(); /// - /// Have the Steam Music player skip to the next song + /// Forces the music player to skip to the next song. /// public static void PlayNext() => Internal?.PlayNext(); /// - /// Gets/Sets the current volume of the Steam Music player + /// Gets and sets the current volume of the Steam Music player /// public static float Volume { @@ -79,4 +86,4 @@ namespace Steamworks set => Internal?.SetVolume( value ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/SteamNetworking.cs index cadac81dd..092e7fe28 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworking.cs @@ -8,15 +8,21 @@ using Steamworks.Data; namespace Steamworks { + /// + /// Class for utilizing the Steam Network API. + /// public class SteamNetworking : SteamSharedClass { internal static ISteamNetworking? Internal => Interface as ISteamNetworking; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamNetworking( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -32,20 +38,20 @@ namespace Steamworks } /// - /// This SteamId wants to send you a message. You should respond by calling AcceptP2PSessionWithUser - /// if you want to recieve their messages + /// Invoked when a wants to send the current user a message. You should respond by calling + /// if you want to recieve their messages. /// public static Action? OnP2PSessionRequest; /// - /// Called when packets can't get through to the specified user. + /// Invoked when packets can't get through to the specified user. /// All queued packets unsent at this point will be dropped, further attempts /// to send will retry making the connection (but will be dropped if we fail again). /// public static Action? OnP2PConnectionFailed; /// - /// This should be called in response to a OnP2PSessionRequest + /// This should be called in response to a . /// public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal != null && Internal.AcceptP2PSessionWithUser( user ); @@ -59,22 +65,31 @@ namespace Steamworks /// /// This should be called when you're done communicating with a user, as this will /// free up all of the resources allocated for the connection under-the-hood. - /// If the remote user tries to send data to you again, a new OnP2PSessionRequest + /// If the remote user tries to send data to you again, a new /// callback will be posted /// public static bool CloseP2PSessionWithUser( SteamId user ) => Internal != null && Internal.CloseP2PSessionWithUser( user ); /// - /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. + /// Checks if a P2P packet is available to read. /// public static bool IsP2PPacketAvailable( int channel = 0 ) { uint _ = 0; return Internal != null && Internal.IsP2PPacketAvailable( ref _, channel ); } + + /// + /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. + /// + public static bool IsP2PPacketAvailable( out uint msgSize, int channel = 0 ) + { + msgSize = 0; + return Internal != null && Internal.IsP2PPacketAvailable( ref msgSize, channel ); + } /// - /// Reads in a packet that has been sent from another user via SendP2PPacket.. + /// Reads in a packet that has been sent from another user via SendP2PPacket. /// public unsafe static P2Packet? ReadP2PPacket( int channel = 0 ) { @@ -103,7 +118,7 @@ namespace Steamworks } /// - /// Reads in a packet that has been sent from another user via SendP2PPacket.. + /// Reads in a packet that has been sent from another user via SendP2PPacket. /// public unsafe static bool ReadP2PPacket( byte[] buffer, ref uint size, ref SteamId steamid, int channel = 0 ) { @@ -113,7 +128,7 @@ namespace Steamworks } /// - /// Reads in a packet that has been sent from another user via SendP2PPacket.. + /// Reads in a packet that has been sent from another user via SendP2PPacket. /// public unsafe static bool ReadP2PPacket( byte* buffer, uint cbuf, ref uint size, ref SteamId steamid, int channel = 0 ) { diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs index 50e80dfd2..02ab4cf54 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs @@ -12,10 +12,32 @@ namespace Steamworks { internal static ISteamNetworkingSockets? Internal => Interface as ISteamNetworkingSockets; - internal override void InitializeInterface( bool server ) + /// + /// Get the identity assigned to this interface. + /// E.g. on Steam, this is the user's SteamID, or for the gameserver interface, the SteamID assigned + /// to the gameserver. Returns false and sets the result to an invalid identity if we don't know + /// our identity yet. (E.g. GameServer has not logged in. On Steam, the user will know their SteamID + /// even if they are not signed into Steam.) + /// + public static NetIdentity Identity + { + get + { + NetIdentity identity = default; + + Internal?.GetIdentity( ref identity ); + + return identity; + } + } + + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamNetworkingSockets( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + return true; } #region SocketInterface @@ -66,6 +88,7 @@ namespace Steamworks internal void InstallEvents( bool server ) { Dispatch.Install( ConnectionStatusChanged, server ); + Dispatch.Install( FakeIPResult, server ); } @@ -90,12 +113,25 @@ namespace Steamworks public static event Action? OnConnectionStatusChanged; + private static void FakeIPResult( SteamNetworkingFakeIPResult_t data ) + { + foreach ( var port in data.Ports ) + { + if ( port == 0 ) continue; + + var address = NetAddress.From( Utility.Int32ToIp( data.IP ), port ); + + OnFakeIPResult?.Invoke( address ); + } + } + + public static event Action? OnFakeIPResult; /// /// Creates a "server" socket that listens for clients to connect to by calling /// Connect, over ordinary UDP (IPv4 or IPv6) /// - /// To use this derive a class from SocketManager and override as much as you want. + /// To use this derive a class from and override as much as you want. /// /// public static T? CreateNormalSocket( NetAddress address ) where T : SocketManager, new() @@ -115,7 +151,7 @@ namespace Steamworks /// Creates a "server" socket that listens for clients to connect to by calling /// Connect, over ordinary UDP (IPv4 or IPv6). /// - /// To use this you should pass a class that inherits ISocketManager. You can use + /// To use this you should pass a class that inherits . You can use /// SocketManager to get connections and send messages, but the ISocketManager class /// will received all the appropriate callbacks. /// @@ -140,7 +176,7 @@ namespace Steamworks } /// - /// Connect to a socket created via CreateListenSocketIP + /// Connect to a socket created via CreateListenSocketIP. /// public static T? ConnectNormal( NetAddress address ) where T : ConnectionManager, new() { @@ -154,7 +190,7 @@ namespace Steamworks } /// - /// Connect to a socket created via CreateListenSocketIP + /// Connect to a socket created via CreateListenSocketIP. /// public static ConnectionManager? ConnectNormal( NetAddress address, IConnectionManager iface ) { @@ -174,9 +210,9 @@ namespace Steamworks } /// - /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping) + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). /// - /// To use this derive a class from SocketManager and override as much as you want. + /// To use this derive a class from and override as much as you want. /// /// public static T? CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() @@ -192,10 +228,10 @@ namespace Steamworks } /// - /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping) + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). /// - /// To use this you should pass a class that inherits ISocketManager. You can use - /// SocketManager to get connections and send messages, but the ISocketManager class + /// To use this you should pass a class that inherits . You can use + /// to get connections and send messages, but the class /// will received all the appropriate callbacks. /// /// @@ -219,7 +255,7 @@ namespace Steamworks } /// - /// Connect to a relay server + /// Connect to a relay server. /// public static T? ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() { @@ -232,5 +268,103 @@ namespace Steamworks SetConnectionManager( t.Connection.Id, t ); return t; } + + /// + /// Connect to a relay server. + /// + public static ConnectionManager? ConnectRelay( SteamId serverId, int virtualport, IConnectionManager iface ) + { + if ( Internal is null ) return null; + + NetIdentity identity = serverId; + var options = Array.Empty(); + var connection = Internal.ConnectP2P( ref identity, virtualport, options.Length, options ); + + var t = new ConnectionManager + { + Connection = connection, + Interface = iface + }; + + SetConnectionManager( t.Connection.Id, t ); + return t; + } + + /// + /// Begin asynchronous process of allocating a fake IPv4 address that other + /// peers can use to contact us via P2P. IP addresses returned by this + /// function are globally unique for a given appid. + /// + /// For gameservers, you *must* call this after initializing the SDK but before + /// beginning login. Steam needs to know in advance that FakeIP will be used. + /// + public static bool RequestFakeIP( int numFakePorts = 1 ) + { + return Internal != null && Internal.BeginAsyncRequestFakeIP( numFakePorts ); + } + + /// + /// Return info about the FakeIP and port that we have been assigned, if any. + /// + /// + public static Result GetFakeIP( int fakePortIndex, out NetAddress address ) + { + if ( Internal is null ) + { + address = default; + return Result.Fail; + } + + var pInfo = default( SteamNetworkingFakeIPResult_t ); + + Internal.GetFakeIP( 0, ref pInfo ); + + address = NetAddress.From( Utility.Int32ToIp( pInfo.IP ), pInfo.Ports[fakePortIndex] ); + return pInfo.Result; + } + + /// + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). + /// + /// To use this derive a class from and override as much as you want. + /// + /// + public static T? CreateRelaySocketFakeIP( int fakePortIndex = 0 ) where T : SocketManager, new() + { + if ( Internal is null ) { return null; } + var t = new T(); + var options = Array.Empty(); + t.Socket = Internal.CreateListenSocketP2PFakeIP( 0, options.Length, options ); + t.Initialize(); + SetSocketManager( t.Socket.Id, t ); + return t; + } + + /// + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). + /// + /// To use this you should pass a class that inherits . You can use + /// to get connections and send messages, but the class + /// will received all the appropriate callbacks. + /// + /// + public static SocketManager? CreateRelaySocketFakeIP( int fakePortIndex, ISocketManager intrface ) + { + if ( Internal is null ) { return null; } + + var options = Array.Empty(); + var socket = Internal.CreateListenSocketP2PFakeIP( 0, options.Length, options ); + + var t = new SocketManager + { + Socket = socket, + Interface = intrface + }; + + t.Initialize(); + + SetSocketManager( t.Socket.Id, t ); + return t; + } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index 319e5d641..4de408956 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -8,16 +8,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Provides Steam Networking utilities. /// public class SteamNetworkingUtils : SteamSharedClass { internal static ISteamNetworkingUtils? Internal => Interface as ISteamNetworkingUtils; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamNetworkingUtils( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallCallbacks( server ); + + return true; } static void InstallCallbacks( bool server ) @@ -36,7 +40,7 @@ namespace Steamworks /// /// A function to receive debug network information on. This will do nothing - /// unless you set DebugLevel to something other than None. + /// unless you set to something other than . /// /// You should set this to an appropriate level instead of setting it to the highest /// and then filtering it by hand because a lot of energy is used by creating the strings @@ -64,21 +68,24 @@ namespace Steamworks /// relay network. If you do not call this, the initialization will /// be delayed until the first time you use a feature that requires access /// to the relay network, which will delay that first access. - /// + /// /// You can also call this to force a retry if the previous attempt has failed. /// Performing any action that requires access to the relay network will also /// trigger a retry, and so calling this function is never strictly necessary, /// but it can be useful to call it a program launch time, if access to the /// relay network is anticipated. - /// + /// + /// /// Use GetRelayNetworkStatus or listen for SteamRelayNetworkStatus_t /// callbacks to know when initialization has completed. /// Typically initialization completes in a few seconds. - /// + /// + /// /// Note: dedicated servers hosted in known data centers do *not* need /// to call this, since they do not make routing decisions. However, if /// the dedicated server will be using P2P functionality, it will act as /// a "client" and this should be called. + /// /// public static void InitRelayNetworkAccess() { @@ -121,7 +128,7 @@ namespace Steamworks /// /// If you need ping information straight away, wait on this. It will return - /// immediately if you already have up to date ping data + /// immediately if you already have up to date ping data. /// public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { @@ -141,7 +148,7 @@ namespace Steamworks /// - /// [0 - 100] - Randomly discard N pct of packets + /// [0 - 100] - Randomly discard N pct of packets. /// public static float FakeSendPacketLoss { @@ -150,7 +157,7 @@ namespace Steamworks } /// - /// [0 - 100] - Randomly discard N pct of packets + /// [0 - 100] - Randomly discard N pct of packets. /// public static float FakeRecvPacketLoss { @@ -159,7 +166,7 @@ namespace Steamworks } /// - /// Delay all packets by N ms + /// Delay all packets by N ms. /// public static float FakeSendPacketLag { @@ -168,7 +175,7 @@ namespace Steamworks } /// - /// Delay all packets by N ms + /// Delay all packets by N ms. /// public static float FakeRecvPacketLag { @@ -177,7 +184,7 @@ namespace Steamworks } /// - /// Timeout value (in ms) to use when first connecting + /// Timeout value (in ms) to use when first connecting. /// public static int ConnectionTimeout { @@ -186,7 +193,7 @@ namespace Steamworks } /// - /// Timeout value (in ms) to use after connection is established + /// Timeout value (in ms) to use after connection is established. /// public static int Timeout { @@ -197,7 +204,7 @@ namespace Steamworks /// /// Upper limit of buffered pending bytes to be sent. /// If this is reached SendMessage will return LimitExceeded. - /// Default is 524288 bytes (512k) + /// Default is 524288 bytes (512k). /// public static int SendBufferSize { @@ -205,17 +212,144 @@ namespace Steamworks set => SetConfigInt( NetConfig.SendBufferSize, value ); } + /// + /// Minimum send rate clamp, 0 is no limit. + /// This value will control the min allowed sending rate that + /// bandwidth estimation is allowed to reach. Default is 0 (no-limit) + /// + public static int SendRateMin + { + get => GetConfigInt( NetConfig.SendRateMin ); + set => SetConfigInt( NetConfig.SendRateMin, value ); + } /// - /// Get Debug Information via OnDebugOutput event - /// - /// Except when debugging, you should only use NetDebugOutput.Msg - /// or NetDebugOutput.Warning. For best performance, do NOT + /// Maximum send rate clamp, 0 is no limit. + /// This value will control the max allowed sending rate that + /// bandwidth estimation is allowed to reach. Default is 0 (no-limit) + /// + public static int SendRateMax + { + get => GetConfigInt( NetConfig.SendRateMax ); + set => SetConfigInt( NetConfig.SendRateMax, value ); + } + + /// + /// Nagle time, in microseconds. When SendMessage is called, if + /// the outgoing message is less than the size of the MTU, it will be + /// queued for a delay equal to the Nagle timer value. This is to ensure + /// that if the application sends several small messages rapidly, they are + /// coalesced into a single packet. + /// See historical RFC 896. Value is in microseconds. + /// Default is 5000us (5ms). + /// + public static int NagleTime + { + get => GetConfigInt( NetConfig.NagleTime ); + set => SetConfigInt( NetConfig.NagleTime, value ); + } + + /// + /// Don't automatically fail IP connections that don't have + /// strong auth. On clients, this means we will attempt the connection even if + /// we don't know our identity or can't get a cert. On the server, it means that + /// we won't automatically reject a connection due to a failure to authenticate. + /// (You can examine the incoming connection and decide whether to accept it.) + /// + /// This is a dev configuration value, and you should not let users modify it in + /// production. + /// + /// + public static int AllowWithoutAuth + { + get => GetConfigInt( NetConfig.IP_AllowWithoutAuth ); + set => SetConfigInt( NetConfig.IP_AllowWithoutAuth, value ); + } + + /// + /// Allow unencrypted (and unauthenticated) communication. + /// 0: Not allowed (the default) + /// 1: Allowed, but prefer encrypted + /// 2: Allowed, and preferred + /// 3: Required. (Fail the connection if the peer requires encryption.) + /// + /// This is a dev configuration value, since its purpose is to disable encryption. + /// You should not let users modify it in production. (But note that it requires + /// the peer to also modify their value in order for encryption to be disabled.) + /// + /// + public static int Unencrypted + { + get => GetConfigInt( NetConfig.Unencrypted ); + set => SetConfigInt( NetConfig.Unencrypted, value ); + } + + /// + /// Log RTT calculations for inline pings and replies. + /// + public static int DebugLevelAckRTT + { + get => GetConfigInt( NetConfig.LogLevel_AckRTT ); + set => SetConfigInt( NetConfig.LogLevel_AckRTT, value ); + } + + /// + /// Log SNP packets send. + /// + public static int DebugLevelPacketDecode + { + get => GetConfigInt( NetConfig.LogLevel_PacketDecode ); + set => SetConfigInt( NetConfig.LogLevel_PacketDecode, value ); + } + + /// + /// Log each message send/recv. + /// + public static int DebugLevelMessage + { + get => GetConfigInt( NetConfig.LogLevel_Message ); + set => SetConfigInt( NetConfig.LogLevel_Message, value ); + } + + /// + /// Log dropped packets. + /// + public static int DebugLevelPacketGaps + { + get => GetConfigInt( NetConfig.LogLevel_PacketGaps ); + set => SetConfigInt( NetConfig.LogLevel_PacketGaps, value ); + } + + /// + /// Log P2P rendezvous messages. + /// + public static int DebugLevelP2PRendezvous + { + get => GetConfigInt( NetConfig.LogLevel_P2PRendezvous ); + set => SetConfigInt( NetConfig.LogLevel_P2PRendezvous, value ); + } + + /// + /// Log ping relays. + /// + public static int DebugLevelSDRRelayPings + { + get => GetConfigInt( NetConfig.LogLevel_SDRRelayPings ); + set => SetConfigInt( NetConfig.LogLevel_SDRRelayPings, value ); + } + + /// + /// Get Debug Information via event. + /// + /// Except when debugging, you should only use + /// or . For best performance, do NOT /// request a high detail level and then filter out messages in the callback. - /// + /// + /// /// This incurs all of the expense of formatting the messages, which are then discarded. /// Setting a high priority value (low numeric value) here allows the library to avoid /// doing this work. + /// /// public static NetDebugOutput DebugLevel { @@ -230,12 +364,12 @@ namespace Steamworks } /// - /// So we can remember and provide a Get for DebugLEvel + /// So we can remember and provide a Get for DebugLevel. /// private static NetDebugOutput _debugLevel; /// - /// We need to keep the delegate around until it's not used anymore + /// We need to keep the delegate around until it's not used anymore. /// static NetDebugFunc? _debugFunc; @@ -256,6 +390,11 @@ namespace Steamworks debugMessages.Enqueue( new DebugMessage { Type = nType, Msg = Helpers.MemoryToString( str ) } ); } + internal static void LogDebugMessage( NetDebugOutput type, string message ) + { + debugMessages.Enqueue( new DebugMessage { Type = type, Msg = message } ); + } + /// /// Called regularly from the Dispatch loop so we can provide a timely /// stream of messages. @@ -271,6 +410,11 @@ namespace Steamworks } } + internal static unsafe NetMsg* AllocateMessage() + { + return Internal != null ? Internal.AllocateMessage(0) : null; + } + #region Config Internals internal unsafe static bool SetConfigInt( NetConfig type, int value ) diff --git a/Libraries/Facepunch.Steamworks/SteamParental.cs b/Libraries/Facepunch.Steamworks/SteamParental.cs index 7f3a23bd6..d110cb301 100644 --- a/Libraries/Facepunch.Steamworks/SteamParental.cs +++ b/Libraries/Facepunch.Steamworks/SteamParental.cs @@ -14,10 +14,14 @@ namespace Steamworks { internal static ISteamParentalSettings? Internal => Interface as ISteamParentalSettings; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamParentalSettings( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -61,4 +65,4 @@ namespace Steamworks /// public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureInBlockList( feature ); } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamParties.cs b/Libraries/Facepunch.Steamworks/SteamParties.cs index 1d7c1eba3..022c3e178 100644 --- a/Libraries/Facepunch.Steamworks/SteamParties.cs +++ b/Libraries/Facepunch.Steamworks/SteamParties.cs @@ -17,10 +17,14 @@ namespace Steamworks { internal static ISteamParties? Internal => Interface as ISteamParties; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamParties( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal void InstallEvents( bool server ) @@ -30,18 +34,23 @@ namespace Steamworks } /// - /// The list of possible Party beacon locations has changed + /// Invoked when the list of possible Party beacon locations has changed /// public static event Action? OnBeaconLocationsUpdated; /// - /// The list of active beacons may have changed + /// Invoked when the list of active beacons may have changed /// public static event Action? OnActiveBeaconsUpdated; - + /// + /// Gets the amount of beacons that are active. + /// public static int ActiveBeaconCount => (int)(Internal?.GetNumActiveBeacons() ?? 0); + /// + /// Gets an of active beacons. + /// public static IEnumerable ActiveBeacons { get @@ -56,4 +65,4 @@ namespace Steamworks } } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs index 502d37291..ebe125b42 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs @@ -14,11 +14,14 @@ namespace Steamworks { internal static ISteamRemotePlay? Internal => Interface as ISteamRemotePlay; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamRemotePlay( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents( server ); + + return true; } internal void InstallEvents( bool server ) @@ -28,30 +31,30 @@ namespace Steamworks } /// - /// Called when a session is connected + /// Invoked when a session is connected. /// public static event Action? OnSessionConnected; /// - /// Called when a session becomes disconnected + /// Invoked when a session becomes disconnected. /// public static event Action? OnSessionDisconnected; /// - /// Get the number of currently connected Steam Remote Play sessions + /// Gets the number of currently connected Steam Remote Play sessions /// public static int SessionCount => (int)(Internal?.GetSessionCount() ?? 0); /// /// Get the currently connected Steam Remote Play session ID at the specified index. - /// IsValid will return false if it's out of bounds + /// IsValid will return if it's out of bounds /// public static RemotePlaySession GetSession( int index ) => Internal?.GetSessionID( index ).Value ?? default; /// - /// Invite a friend to Remote Play Together - /// This returns false if the invite can't be sent + /// Invite a friend to Remote Play Together. + /// This returns if the invite can't be sent /// public static bool SendInvite( SteamId steamid ) => Internal != null && Internal.BSendRemotePlayTogetherInvite( steamid ); } diff --git a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs index 847147680..02d28e951 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs @@ -8,22 +8,28 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Remote Storage API. /// public class SteamRemoteStorage : SteamClientClass { internal static ISteamRemoteStorage? Internal => Interface as ISteamRemoteStorage; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamRemoteStorage( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } - + /// /// Creates a new file, writes the bytes to the file, and then closes the file. /// If the target file already exists, it is overwritten /// + /// The path of the file. + /// The bytes of data. + /// A boolean, detailing whether or not the operation was successful. public unsafe static bool FileWrite( string filename, byte[] data ) { fixed ( byte* ptr = data ) @@ -35,6 +41,7 @@ namespace Steamworks /// /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. /// + /// The path of the file. public unsafe static byte[]? FileRead( string filename ) { if (Internal is null) { return null; } @@ -46,6 +53,10 @@ namespace Steamworks fixed ( byte* ptr = buffer ) { var readsize = Internal.FileRead( filename, (IntPtr)ptr, size ); + if ( readsize != size ) + { + return null; + } return buffer; } } @@ -53,26 +64,35 @@ namespace Steamworks /// /// Checks whether the specified file exists. /// + /// The path of the file. + /// Whether or not the file exists. public static bool FileExists( string filename ) => Internal != null && Internal.FileExists( filename ); /// /// Checks if a specific file is persisted in the steam cloud. /// + /// The path of the file. + /// Boolean. public static bool FilePersisted( string filename ) => Internal != null && Internal.FilePersisted( filename ); /// /// Gets the specified file's last modified date/time. /// + /// The path of the file. + /// A describing when the file was modified last. public static DateTime FileTime( string filename ) => Internal != null ? Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ) : default; /// - /// Gets the specified files size in bytes. 0 if not exists. + /// Returns the specified files size in bytes, or 0 if the file does not exist. /// + /// The path of the file. + /// The size of the file in bytes, or 0 if the file doesn't exist. public static int FileSize( string filename ) => Internal?.GetFileSize( filename ) ?? 0; /// /// Deletes the file from remote storage, but leaves it on the local disk and remains accessible from the API. /// + /// A boolean, detailing whether or not the operation was successful. public static bool FileForget( string filename ) => Internal != null && Internal.FileForget( filename ); /// @@ -82,7 +102,7 @@ namespace Steamworks /// - /// Number of bytes total + /// Gets the total number of quota bytes. /// public static ulong QuotaBytes { @@ -95,7 +115,7 @@ namespace Steamworks } /// - /// Number of bytes used + /// Gets the total number of quota bytes that have been used. /// public static ulong QuotaUsedBytes { @@ -108,7 +128,7 @@ namespace Steamworks } /// - /// Number of bytes remaining until your quota is used + /// Number of bytes remaining until the quota is used. /// public static ulong QuotaRemainingBytes { @@ -121,7 +141,7 @@ namespace Steamworks } /// - /// returns true if IsCloudEnabledForAccount AND IsCloudEnabledForApp + /// returns if AND are . /// public static bool IsCloudEnabled => IsCloudEnabledForAccount && IsCloudEnabledForApp; @@ -162,7 +182,7 @@ namespace Steamworks } /// - /// Get a list of filenames synchronized by Steam Cloud + /// Gets a list of filenames synchronized by Steam Cloud. /// public static List Files { @@ -182,4 +202,4 @@ namespace Steamworks } } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamScreenshots.cs b/Libraries/Facepunch.Steamworks/SteamScreenshots.cs index 2c91a7eb4..36b322f87 100644 --- a/Libraries/Facepunch.Steamworks/SteamScreenshots.cs +++ b/Libraries/Facepunch.Steamworks/SteamScreenshots.cs @@ -8,16 +8,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Screenshots API. /// public class SteamScreenshots : SteamClientClass { internal static ISteamScreenshots? Internal => Interface as ISteamScreenshots; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamScreenshots( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); + + return true; } internal static void InstallEvents() @@ -33,19 +37,19 @@ namespace Steamworks } /// - /// A screenshot has been requested by the user from the Steam screenshot hotkey. - /// This will only be called if Hooked is true, in which case Steam + /// Invoked when a screenshot has been requested by the user from the Steam screenshot hotkey. + /// This will only be called if is true, in which case Steam /// will not take the screenshot itself. /// public static event Action? OnScreenshotRequested; /// - /// A screenshot successfully written or otherwise added to the library and can now be tagged. + /// Invoked when a screenshot has been successfully written or otherwise added to the library and can now be tagged. /// public static event Action? OnScreenshotReady; /// - /// A screenshot attempt failed + /// Invoked when a screenshot attempt failed. /// public static event Action? OnScreenshotFailed; @@ -85,15 +89,17 @@ namespace Steamworks /// /// Causes the Steam overlay to take a screenshot. /// If screenshots are being hooked by the game then a - /// ScreenshotRequested callback is sent back to the game instead. + /// callback is sent back to the game instead. /// public static void TriggerScreenshot() => Internal?.TriggerScreenshot(); /// /// Toggles whether the overlay handles screenshots when the user presses the screenshot hotkey, or if the game handles them. + /// /// Hooking is disabled by default, and only ever enabled if you do so with this function. - /// If the hooking is enabled, then the ScreenshotRequested_t callback will be sent if the user presses the hotkey or - /// when TriggerScreenshot is called, and then the game is expected to call WriteScreenshot or AddScreenshotToLibrary in response. + /// If the hooking is enabled, then the callback will be sent if the user presses the hotkey or + /// when TriggerScreenshot is called, and then the game is expected to call or in response. + /// /// public static bool Hooked { @@ -101,4 +107,4 @@ namespace Steamworks set => Internal?.HookScreenshots( value ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index b3ea1bef5..d2a264969 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -15,10 +15,14 @@ namespace Steamworks { internal static ISteamGameServer? Internal => Interface as ISteamGameServer; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamGameServer( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); + + return true; } public static bool IsValid => Internal != null && Internal.IsValid; @@ -33,35 +37,35 @@ namespace Steamworks } /// - /// User has been authed or rejected + /// Invoked when aser has been authed or rejected /// public static event Action? OnValidateAuthTicketResponse; /// - /// Called when a connections to the Steam back-end has been established. + /// Invoked when a connection to the Steam back-end has been established. /// This means the server now is logged on and has a working connection to the Steam master server. /// public static event Action? OnSteamServersConnected; /// - /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying) + /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying). /// public static event Action? OnSteamServerConnectFailure; /// - /// Disconnected from Steam + /// Invoked when the server is disconnected from Steam /// public static event Action? OnSteamServersDisconnected; /// - /// Called when authentication status changes, useful for grabbing SteamId once aavailability is current + /// Invoked when authentication status changes, useful for grabbing once availability is current. /// public static event Action? OnSteamNetAuthenticationStatus; /// /// Initialize the steam server. - /// If asyncCallbacks is false you need to call RunCallbacks manually every frame. + /// If is you need to call manually every frame. /// public static void Init( AppId appid, SteamServerInit init, bool asyncCallbacks = true ) { @@ -70,9 +74,6 @@ namespace Steamworks uint ipaddress = 0; // Any Port - if ( init.SteamPort == 0 ) - init = init.WithRandomSteamPort(); - if ( init.IpAddress != null ) ipaddress = Utility.IpToInt32( init.IpAddress ); @@ -82,9 +83,9 @@ namespace Steamworks // // Get other interfaces // - if ( !SteamInternal.GameServer_Init( ipaddress, init.SteamPort, init.GamePort, init.QueryPort, (int)init.Mode, init.VersionString ) ) + if ( !SteamInternal.GameServer_Init( ipaddress, 0, init.GamePort, init.QueryPort, (int)init.Mode, init.VersionString ) ) { - throw new System.Exception( $"InitGameServer returned false ({ipaddress},{init.SteamPort},{init.GamePort},{init.QueryPort},{init.Mode},\"{init.VersionString}\")" ); + throw new System.Exception( $"InitGameServer returned false ({ipaddress},{0},{init.GamePort},{init.QueryPort},{init.Mode},\"{init.VersionString}\")" ); } // @@ -109,7 +110,7 @@ namespace Steamworks // // Initial settings // - AutomaticHeartbeats = true; + AdvertiseServer = true; MaxPlayers = 32; BotCount = 0; Product = $"{appid.Value}"; @@ -210,7 +211,7 @@ namespace Steamworks private static string _mapname = ""; /// - /// Gets or sets the current ModDir + /// Gets or sets the current ModDir. /// public static string ModDir { @@ -220,7 +221,7 @@ namespace Steamworks private static string _modDir = ""; /// - /// Gets the current product + /// Gets the current product. /// public static string Product { @@ -230,7 +231,7 @@ namespace Steamworks private static string _product = ""; /// - /// Gets or sets the current Product + /// Gets or sets the current Product. /// public static string GameDescription { @@ -240,7 +241,7 @@ namespace Steamworks private static string _gameDescription = ""; /// - /// Gets or sets the current ServerName + /// Gets or sets the current ServerName. /// public static string ServerName { @@ -250,7 +251,7 @@ namespace Steamworks private static string _serverName = ""; /// - /// Set whether the server should report itself as passworded + /// Set whether the server should report itself as passworded. /// public static bool Passworded { @@ -275,6 +276,9 @@ namespace Steamworks } private static string _gametags = ""; + /// + /// Gets the SteamId of the server. + /// public static SteamId SteamId => Internal?.GetSteamID() ?? default; /// @@ -287,7 +291,7 @@ namespace Steamworks } /// - /// Log onto Steam anonymously. + /// Log off of Steam. /// public static void LogOff() { @@ -296,14 +300,14 @@ namespace Steamworks /// /// Returns true if the server is connected and registered with the Steam master server - /// You should have called LogOnAnonymous etc on startup. + /// You should have called etc on startup. /// public static bool LoggedOn => Internal != null && Internal.BLoggedOn(); /// /// To the best of its ability this tries to get the server's - /// current public ip address. Be aware that this is likely to return - /// null for the first few seconds after initialization. + /// current public IP address. Be aware that this is likely to return + /// for the first few seconds after initialization. /// public static System.Net.IPAddress? PublicIp => Internal?.GetPublicIP(); @@ -311,27 +315,29 @@ namespace Steamworks /// Enable or disable heartbeats, which are sent regularly to the master server. /// Enabled by default. /// + [Obsolete( "Renamed to AdvertiseServer in 1.52" )] public static bool AutomaticHeartbeats { - set { Internal?.EnableHeartbeats( value ); } - } + set { Internal?.SetAdvertiseServerActive( value ); } + } + /// - /// Set heartbeat interval, if automatic heartbeats are enabled. - /// You can leave this at the default. + /// Enable or disable heartbeats, which are sent regularly to the master server. + /// Enabled by default. /// - public static int AutomaticHeartbeatRate + public static bool AdvertiseServer { - set { Internal?.SetHeartbeatInterval( value ); } + set { Internal?.SetAdvertiseServerActive( value ); } } /// /// Force send a heartbeat to the master server instead of waiting /// for the next automatic update (if you've left them enabled) /// + [Obsolete( "No longer used" )] public static void ForceHeartbeat() { - Internal?.ForceHeartbeat(); } /// @@ -372,7 +378,7 @@ namespace Steamworks } /// - /// Remove all key values + /// Remove all key values. /// public static void ClearKeys() { @@ -456,11 +462,11 @@ namespace Steamworks } /// - /// Does the user own this app (which could be DLC) + /// Does the user own this app (which could be DLC). /// public static UserHasLicenseForAppResult UserHasLicenseForApp( SteamId steamid, AppId appid ) { return Internal?.UserHasLicenseForApp( steamid, appid ) ?? UserHasLicenseForAppResult.NoAuth; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamServerStats.cs b/Libraries/Facepunch.Steamworks/SteamServerStats.cs index 823a92771..86232fb0c 100644 --- a/Libraries/Facepunch.Steamworks/SteamServerStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamServerStats.cs @@ -11,17 +11,22 @@ namespace Steamworks { internal static ISteamGameServerStats? Internal => Interface as ISteamGameServerStats; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamGameServerStats( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } /// - /// Downloads stats for the user - /// If the user has no stats will return fail - /// these stats will only be auto-updated for clients playing on the server + /// Downloads stats for the user. + /// If the user has no stats, this will return . + /// These stats will only be auto-updated for clients playing on the server. /// + /// The SteamId of the user to get stats for. + /// A task describing the progress and result of the download. public static async Task RequestUserStatsAsync( SteamId steamid ) { if (Internal is null) { return Result.Fail; } @@ -34,6 +39,9 @@ namespace Steamworks /// Set the named stat for this user. Setting stats should follow the rules /// you defined in Steamworks. /// + /// The SteamId of the user to set stats for. + /// The name of the stat. + /// The value of the stat. public static bool SetInt( SteamId steamid, string name, int stat ) { return Internal != null && Internal.SetUserStat( steamid, name, stat ); @@ -43,6 +51,9 @@ namespace Steamworks /// Set the named stat for this user. Setting stats should follow the rules /// you defined in Steamworks. /// + /// The SteamId of the user to set stats for. + /// The name of the stat. + /// The value of the stat. public static bool SetFloat( SteamId steamid, string name, float stat ) { return Internal != null && Internal.SetUserStat( steamid, name, stat ); @@ -50,9 +61,12 @@ namespace Steamworks /// /// Get the named stat for this user. If getting the stat failed, will return - /// defaultValue. You should have called Refresh for this userid - which downloads - /// the stats from the backend. If you didn't call it this will always return defaultValue. + /// . You should have called for this SteamID - which downloads + /// the stats from the backend. If you didn't call it this will always return . /// + /// The SteamId of the user to get stats for. + /// The name of the stat. + /// The value to return if the stats cannot be received. public static int GetInt( SteamId steamid, string name, int defaultValue = 0 ) { int data = defaultValue; @@ -68,6 +82,9 @@ namespace Steamworks /// defaultValue. You should have called Refresh for this userid - which downloads /// the stats from the backend. If you didn't call it this will always return defaultValue. /// + /// The SteamId of the user to get stats for. + /// The name of the stat. + /// The value to return if the stats cannot be received. public static float GetFloat( SteamId steamid, string name, float defaultValue = 0 ) { float data = defaultValue; @@ -79,25 +96,29 @@ namespace Steamworks } /// - /// Unlocks the specified achievement for the specified user. Must have called Refresh on a steamid first. - /// Remember to use Commit after use. + /// Unlocks the specified achievement for the specified user. Must have called on a SteamID first. + /// Remember to use after use. /// + /// The SteamId of the user to unlock the achievement for. + /// The ID of the achievement. public static bool SetAchievement( SteamId steamid, string name ) { return Internal != null && Internal.SetUserAchievement( steamid, name ); } /// - /// Resets the unlock status of an achievement for the specified user. Must have called Refresh on a steamid first. - /// Remember to use Commit after use. + /// Resets the unlock status of an achievement for the specified user. Must have called on a SteamID first. + /// Remember to use after use. /// + /// The SteamId of the user to clear the achievement for. + /// The ID of the achievement. public static bool ClearAchievement( SteamId steamid, string name ) { return Internal != null && Internal.ClearUserAchievement( steamid, name ); } /// - /// Return true if available, exists and unlocked + /// Return if available, exists and unlocked /// public static bool GetAchievement( SteamId steamid, string name ) { @@ -111,9 +132,11 @@ namespace Steamworks /// /// Once you've set a stat change on a user you need to commit your changes. - /// You can do that using this function. The callback will let you know if + /// You can do that using this method. The callback will let you know if /// your action succeeded, but most of the time you can fire and forget. /// + /// The SteamId of the user to store stats for. + /// A task describing the progress and result of the commit. public static async Task StoreUserStats( SteamId steamid ) { if (Internal is null) { return Result.Fail; } @@ -122,4 +145,4 @@ namespace Steamworks return r.Value.Result; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index ecf20286e..af6f1c36c 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -17,34 +17,59 @@ namespace Steamworks { internal static ISteamUGC? Internal => Interface as ISteamUGC; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUGC( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) { Dispatch.Install( x => { - if (x.AppID == SteamClient.AppId) + if ( x.AppID == SteamClient.AppId ) { - OnDownloadItemResult?.Invoke(x.Result, x.PublishedFileId); - } - }, server ); - Dispatch.Install(x => - { - if (x.AppID == SteamClient.AppId) - { - GlobalOnItemInstalled?.Invoke(x.PublishedFileId); + OnDownloadItemResult?.Invoke( x.Result, x.PublishedFileId ); } }, server); + Dispatch.Install( x => + { + if ( x.AppID == SteamClient.AppId ) + { + OnItemSubscribed?.Invoke( x.AppID.Value, x.PublishedFileId ); + } + }, server ); + Dispatch.Install( x => + { + if ( x.AppID == SteamClient.AppId ) + { + OnItemUnsubscribed?.Invoke( x.AppID.Value, x.PublishedFileId ); + } + }, server ); + Dispatch.Install( x => + { + if ( x.AppID == SteamClient.AppId ) + { + OnItemInstalled?.Invoke( x.AppID.Value, x.PublishedFileId ); + } + }, server ); } /// - /// Posted after Download call + /// Invoked after an item is downloaded. /// - public static event Action? OnDownloadItemResult; + public static event Action? OnDownloadItemResult; + + /// + /// Invoked when a new item is subscribed. + /// + public static event Action? OnItemSubscribed; + public static event Action? OnItemUnsubscribed; + public static event Action? OnItemInstalled; public static async Task DeleteFileAsync( PublishedFileId fileId ) { @@ -54,29 +79,29 @@ namespace Steamworks } /// - /// Start downloading this item. You'll get notified of completion via OnDownloadItemResult. + /// Start downloading this item. You'll get notified of completion via . /// - /// The ID of the file you want to download - /// If true this should go straight to the top of the download list - /// true if nothing went wrong and the download is started + /// The ID of the file to download. + /// If this should go straight to the top of the download list. + /// if nothing went wrong and the download is started. public static bool Download( PublishedFileId fileId, bool highPriority = false ) { return Internal != null && Internal.DownloadItem( fileId, highPriority ); } /// - /// Will attempt to download this item asyncronously - allowing you to instantly react to its installation + /// Will attempt to download this item asyncronously - allowing you to instantly react to its installation. /// - /// The ID of the file you want to download + /// The ID of the file you download. /// An optional callback - /// Allows you to send a message to cancel the download anywhere during the process - /// How often to call the progress function - /// true if downloaded and installed correctly + /// Allows to send a message to cancel the download anywhere during the process. + /// How often to call the progress function. + /// if downloaded and installed properly. public static async Task DownloadAsync( - PublishedFileId fileId, - Action? progress = null, - int millisecondsUpdateDelay = 60, - CancellationToken? ct = null) + PublishedFileId fileId, + Action? progress = null, + int millisecondsUpdateDelay = 60, + CancellationToken? ct = default ) { var item = new Steamworks.Ugc.Item( fileId ); @@ -92,7 +117,7 @@ namespace Steamworks Result downloadStartResult = Result.None; - void onDownloadFinished(Result r, ulong id) + void onDownloadFinished(Result r, PublishedFileId id) { if (id != item.Id) { return; } downloadStartResult = r; @@ -143,7 +168,7 @@ namespace Steamworks } /// - /// Utility function to fetch a single item. Internally this uses Ugc.FileQuery - + /// Utility function to fetch a single item. Internally this uses Ugc.FileQuery - /// which you can use to query multiple items if you need to. /// public static async Task QueryFileAsync( PublishedFileId fileId ) @@ -183,8 +208,6 @@ namespace Steamworks return result?.Result == Result.OK; } - public static Action? GlobalOnItemInstalled; - public static uint NumSubscribedItems { get { return Internal?.GetNumSubscribedItems() ?? 0; } } public static PublishedFileId[] GetSubscribedItems() @@ -195,6 +218,36 @@ namespace Steamworks Internal.GetSubscribedItems(ids, numSubscribed); return ids; } + + /// + /// Suspends all workshop downloads. + /// Downloads will be suspended until you resume them by calling or when the game ends. + /// + public static void SuspendDownloads() => Internal?.SuspendDownloads(true); + + /// + /// Resumes all workshop downloads. + /// + public static void ResumeDownloads() => Internal?.SuspendDownloads(false); + + /// + /// Show the app's latest Workshop EULA to the user in an overlay window, where they can accept it or not. + /// + public static bool ShowWorkshopEula() + { + return Internal != null && Internal.ShowWorkshopEULA(); + } + + /// + /// Retrieve information related to the user's acceptance or not of the app's specific Workshop EULA. + /// + public static async Task GetWorkshopEulaStatus() + { + if ( Internal is null ) { return null; } + var status = await Internal.GetWorkshopEULAStatus(); + return status?.Accepted; + } + } } diff --git a/Libraries/Facepunch.Steamworks/SteamUser.cs b/Libraries/Facepunch.Steamworks/SteamUser.cs index 8f4c6620e..a0afe522a 100644 --- a/Libraries/Facepunch.Steamworks/SteamUser.cs +++ b/Libraries/Facepunch.Steamworks/SteamUser.cs @@ -17,13 +17,17 @@ namespace Steamworks { internal static ISteamUser? Internal => Interface as ISteamUser; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUser( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); richPresence = new Dictionary(); SampleRate = OptimalSampleRate; + + return true; } static Dictionary? richPresence; @@ -39,11 +43,12 @@ namespace Steamworks Dispatch.Install( x => OnMicroTxnAuthorizationResponse?.Invoke( x.AppID, x.OrderID, x.Authorized != 0 ) ); Dispatch.Install( x => OnGameWebCallback?.Invoke( x.URLUTF8() ) ); Dispatch.Install( x => OnGetAuthSessionTicketResponse?.Invoke( x ) ); + Dispatch.Install( x => OnGetAuthTicketForWebApiResponse?.Invoke( x ) ); Dispatch.Install( x => OnDurationControl?.Invoke( new DurationControl { _inner = x } ) ); } /// - /// Called when a connections to the Steam back-end has been established. + /// Invoked when a connections to the Steam back-end has been established. /// This means the Steam client now has a working connection to the Steam servers. /// Usually this will have occurred before the game has launched, and should only be seen if the /// user has dropped connection due to a networking issue or a Steam server update. @@ -51,14 +56,14 @@ namespace Steamworks public static event Action? OnSteamServersConnected; /// - /// Called when a connection attempt has failed. + /// Invoked when a connection attempt has failed. /// This will occur periodically if the Steam client is not connected, /// and has failed when retrying to establish a connection. /// public static event Action? OnSteamServerConnectFailure; /// - /// Called if the client has lost connection to the Steam servers. + /// Invoked when the client has lost connection to the Steam servers. /// Real-time services will be disabled until a matching OnSteamServersConnected has been posted. /// public static event Action? OnSteamServersDisconnected; @@ -72,25 +77,30 @@ namespace Steamworks public static event Action? OnClientGameServerDeny; /// - /// Called whenever the users licenses (owned packages) changes. + /// Invoked whenever the users licenses (owned packages) changes. /// public static event Action? OnLicensesUpdated; /// - /// Called when an auth ticket has been validated. - /// The first parameter is the steamid of this user - /// The second is the Steam ID that owns the game, this will be different from the first - /// if the game is being borrowed via Steam Family Sharing + /// Invoked when an auth ticket has been validated. + /// The first parameter is the of this user + /// The second is the that owns the game, which will be different from the first + /// if the game is being borrowed via Steam Family Sharing. /// public static event Action? OnValidateAuthTicketResponse; /// - /// Used internally for GetAuthSessionTicketAsync + /// Used internally for . /// internal static event Action? OnGetAuthSessionTicketResponse; + + /// + /// Used internally for . + /// + internal static event Action? OnGetAuthTicketForWebApiResponse; /// - /// Called when a user has responded to a microtransaction authorization request. + /// Invoked when a user has responded to a microtransaction authorization request. /// ( appid, orderid, user authorized ) /// public static event Action? OnMicroTxnAuthorizationResponse; @@ -110,9 +120,6 @@ namespace Steamworks /// public static event Action? OnDurationControl; - - - static bool _recordingVoice; /// @@ -120,7 +127,6 @@ namespace Steamworks /// Once started, use GetAvailableVoice and GetVoice to get the data, and then call StopVoiceRecording /// when the user has released their push-to-talk hotkey or the game session has completed. /// - public static bool VoiceRecord { get => _recordingVoice; @@ -134,7 +140,7 @@ namespace Steamworks /// - /// Returns true if we have voice data waiting to be read + /// Returns true if we have voice data waiting to be read. /// public static bool HasVoiceData { @@ -304,16 +310,16 @@ namespace Steamworks } /// - /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. + /// Retrieve an authentication ticket to be sent to the entity who wishes to authenticate you. /// - public static unsafe AuthTicket? GetAuthSessionTicket() + public static unsafe AuthTicket? GetAuthSessionTicket( NetIdentity identity ) { var data = Helpers.TakeBuffer( 1024 ); fixed ( byte* b = data ) { uint ticketLength = 0; - uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ) ?? 0; + uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength, ref identity ) ?? 0; if ( ticket == 0 ) return null; @@ -326,13 +332,56 @@ namespace Steamworks } } + public static async Task GetAuthTicketForWebApi( string identity ) + { + if ( Internal is null ) { return null; } + + HAuthTicket handle = default; + AuthTicketForWebApi? ticket = null; + Result result = Result.Pending; + + Action responseHandler = response => + { + if ( response.Ticket == handle ) { return; } + + result = response.Result == Result.Pending + ? Result.Fail + : response.Result; + ticket = result == Result.OK + ? new AuthTicketForWebApi( + response.GubTicket.Take( response.Ticket ).ToArray(), + response.AuthTicket ) + : null; + }; + + OnGetAuthTicketForWebApiResponse += responseHandler; + try + { + handle = Internal.GetAuthTicketForWebApi( identity ); + + if ( handle == 0 ) { return null; } + + var timeout = DateTime.Now + TimeSpan.FromSeconds( 60f ); + while ( result == Result.Pending && DateTime.Now < timeout ) + { + await Task.Delay( 10 ); + } + } + finally + { + OnGetAuthTicketForWebApiResponse -= responseHandler; + } + + return ticket; + } + /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// This waits for a positive response from the backend before returning the ticket. This means - /// the ticket is definitely ready to go as soon as it returns. Will return null if the callback + /// the ticket is definitely ready to go as soon as it returns. Will return if the callback /// times out or returns negatively. /// - public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) + public static async Task GetAuthSessionTicketAsync( NetIdentity identity, double timeoutSeconds = 10.0f ) { var result = Result.Pending; AuthTicket? ticket = null; @@ -348,7 +397,7 @@ namespace Steamworks try { - ticket = GetAuthSessionTicket(); + ticket = GetAuthSessionTicket( identity ); if ( ticket == null ) return null; @@ -518,4 +567,4 @@ namespace Steamworks return new DurationControl { _inner = response.Value }; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamUserStats.cs b/Libraries/Facepunch.Steamworks/SteamUserStats.cs index f9fc6fa5d..bf0a267a0 100644 --- a/Libraries/Facepunch.Steamworks/SteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamUserStats.cs @@ -11,11 +11,15 @@ namespace Steamworks { internal static ISteamUserStats? Internal => Interface as ISteamUserStats; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUserStats( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); RequestCurrentStats(); + + return true; } public static bool StatsRecieved { get; internal set; } @@ -38,25 +42,25 @@ namespace Steamworks /// - /// called when the achivement icon is loaded + /// Invoked when an achivement icon is loaded. /// internal static event Action? OnAchievementIconFetched; /// - /// called when the latests stats and achievements have been received - /// from the server + /// Invoked when the latests stats and achievements have been received + /// from the server. /// public static event Action? OnUserStatsReceived; /// - /// result of a request to store the user stats for a game + /// Result of a request to store the user stats for a game. /// public static event Action? OnUserStatsStored; /// - /// result of a request to store the achievements for a game, or an + /// Result of a request to store the achievements for a game, or an /// "indicate progress" call. If both m_nCurProgress and m_nMaxProgress - /// are zero, that means the achievement has been fully unlocked + /// are zero, that means the achievement has been fully unlocked. /// public static event Action? OnAchievementProgress; @@ -67,7 +71,7 @@ namespace Steamworks public static event Action? OnUserStatsUnloaded; /// - /// Get the available achievements + /// Get all available achievements. /// public static IEnumerable Achievements { @@ -104,7 +108,7 @@ namespace Steamworks /// /// Tries to get the number of players currently playing this game. - /// Or -1 if failed. + /// Or -1 if failed. /// public static async Task PlayerCountAsync() { @@ -146,11 +150,11 @@ namespace Steamworks /// /// Asynchronously fetches global stats data, which is available for stats marked as /// "aggregated" in the App Admin panel of the Steamworks website. - /// You must have called RequestCurrentStats and it needs to return successfully via + /// You must have called and it needs to return successfully via /// its callback prior to calling this. /// - /// How many days of day-by-day history to retrieve in addition to the overall totals. The limit is 60. - /// OK indicates success, InvalidState means you need to call RequestCurrentStats first, Fail means the remote call failed + /// How many days of day-by-day history to retrieve in addition to the overall totals. The limit is 60. + /// indicates success, means you need to call first, means the remote call failed public static async Task RequestGlobalStatsAsync( int days ) { if (Internal is null) { return Result.Fail; } @@ -216,8 +220,7 @@ namespace Steamworks } /// - /// Set a stat value. This will automatically call StoreStats() after a successful call - /// unless you pass false as the last argument. + /// Set a stat value. This will automatically call after a successful call. /// public static bool SetStat( string name, int value ) { @@ -225,8 +228,7 @@ namespace Steamworks } /// - /// Set a stat value. This will automatically call StoreStats() after a successful call - /// unless you pass false as the last argument. + /// Set a stat value. This will automatically call after a successful call. /// public static bool SetStat( string name, float value ) { @@ -234,7 +236,7 @@ namespace Steamworks } /// - /// Get a Int stat value + /// Get an stat value. /// public static int GetStatInt( string name ) { @@ -244,7 +246,7 @@ namespace Steamworks } /// - /// Get a float stat value + /// Get a stat value. /// public static float GetStatFloat( string name ) { @@ -254,7 +256,7 @@ namespace Steamworks } /// - /// Practically wipes the slate clean for this user. If includeAchievements is true, will wipe + /// Practically wipes the slate clean for this user. If is , will also wipe /// any achievements too. /// /// @@ -263,4 +265,4 @@ namespace Steamworks return Internal != null && Internal.ResetAllStats( includeAchievements ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamUtils.cs b/Libraries/Facepunch.Steamworks/SteamUtils.cs index 3f1553c23..94bd90158 100644 --- a/Libraries/Facepunch.Steamworks/SteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamUtils.cs @@ -14,10 +14,14 @@ namespace Steamworks { internal static ISteamUtils? Internal => Interface as ISteamUtils; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUtils( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -36,33 +40,33 @@ namespace Steamworks } /// - /// The country of the user changed + /// Invoked when the country of the user changed. /// public static event Action? OnIpCountryChanged; /// - /// Fired when running on a laptop and less than 10 minutes of battery is left, fires then every minute - /// The parameter is the number of minutes left + /// Invoked when running on a laptop and less than 10 minutes of battery is left, fires then every minute. + /// The parameter is the number of minutes left. /// public static event Action? OnLowBatteryPower; /// - /// Called when Steam wants to shutdown + /// Invoked when Steam wants to shutdown. /// public static event Action? OnSteamShutdown; /// - /// Big Picture gamepad text input has been closed. Parameter is true if text was submitted, false if cancelled etc. + /// Invoked when Big Picture gamepad text input has been closed. Parameter is if text was submitted, if cancelled etc. /// public static event Action? OnGamepadTextInputDismissed; /// - /// Returns the number of seconds since the application was active + /// Returns the number of seconds since the application was active. /// public static uint SecondsSinceAppActive => Internal?.GetSecondsSinceAppActive() ?? 0; /// - /// Returns the number of seconds since the user last moved the mouse etc + /// Returns the number of seconds since the user last moved the mouse and/or provided other input. /// public static uint SecondsSinceComputerActive => Internal?.GetSecondsSinceComputerActive() ?? 0; @@ -70,7 +74,7 @@ namespace Steamworks public static Universe ConnectedUniverse => Internal?.GetConnectedUniverse() ?? Universe.Invalid; /// - /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) + /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) /// public static DateTime SteamServerTime => Internal != null ? Epoch.ToDateTime( Internal.GetServerRealTime() ) : default; @@ -81,9 +85,9 @@ namespace Steamworks public static string? IpCountry => Internal?.GetIPCountry(); /// - /// returns true if the image exists, and the buffer was successfully filled out - /// results are returned in RGBA format - /// the destination buffer size should be 4 * height * width * sizeof(char) + /// Returns true if the image exists, and the buffer was successfully filled out. + /// Results are returned in RGBA format. + /// The destination buffer size should be 4 * height * width * sizeof(char). /// public static bool GetImageSize( int image, out uint width, out uint height ) { @@ -93,7 +97,7 @@ namespace Steamworks } /// - /// returns the image in RGBA format + /// returns the image in RGBA format. /// public static Data.Image? GetImage( int image ) { @@ -118,12 +122,12 @@ namespace Steamworks } /// - /// Returns true if we're using a battery (ie, a laptop not plugged in) + /// Returns true if we're using a battery (ie, a laptop not plugged in). /// public static bool UsingBatteryPower => Internal != null && Internal.GetCurrentBatteryPower() != 255; /// - /// Returns battery power [0-1] + /// Returns battery power [0-1]. /// public static float CurrentBatteryPower => Math.Min( (Internal?.GetCurrentBatteryPower() ?? 0f) / 100, 1.0f ); @@ -182,7 +186,7 @@ namespace Steamworks } /// - /// Activates the Big Picture text input dialog which only supports gamepad input + /// Activates the Big Picture text input dialog which only supports gamepad input. /// public static bool ShowGamepadTextInput( GamepadTextInputMode inputMode, GamepadTextInputLineMode lineInputMode, string description, int maxChars, string existingText = "" ) { @@ -190,7 +194,7 @@ namespace Steamworks } /// - /// Returns previously entered text + /// Returns previously entered text. /// public static string GetEnteredGamepadText() { @@ -206,18 +210,18 @@ namespace Steamworks } /// - /// returns the language the steam client is running in, you probably want - /// Apps.CurrentGameLanguage instead, this is for very special usage cases + /// Returns the language the steam client is running in. You probably want + /// instead, this is for very special usage cases. /// public static string? SteamUILanguage => Internal?.GetSteamUILanguage(); /// - /// returns true if Steam itself is running in VR mode + /// Returns if Steam itself is running in VR mode. /// public static bool IsSteamRunningInVR => Internal != null && Internal.IsSteamRunningInVR(); /// - /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition + /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition. /// public static void SetOverlayNotificationInset( int x, int y ) { @@ -225,24 +229,26 @@ namespace Steamworks } /// - /// returns true if Steam and the Steam Overlay are running in Big Picture mode + /// returns if Steam and the Steam Overlay are running in Big Picture mode /// Games much be launched through the Steam client to enable the Big Picture overlay. During development, - /// a game can be added as a non-steam game to the developers library to test this feature + /// a game can be added as a non-steam game to the developers library to test this feature. /// public static bool IsSteamInBigPictureMode => Internal != null && Internal.IsSteamInBigPictureMode(); /// - /// ask SteamUI to create and render its OpenVR dashboard + /// Ask Steam UI to create and render its OpenVR dashboard. /// public static void StartVRDashboard() => Internal?.StartVRDashboard(); /// - /// Set whether the HMD content will be streamed via Steam In-Home Streaming - /// If this is set to true, then the scene in the HMD headset will be streamed, and remote input will not be allowed. - /// If this is set to false, then the application window will be streamed instead, and remote input will be allowed. - /// The default is true unless "VRHeadsetStreaming" "0" is in the extended appinfo for a game. - /// (this is useful for games that have asymmetric multiplayer gameplay) + /// Gets or sets whether the HMD content will be streamed via Steam In-Home Streaming. + /// + /// If this is set to , then the scene in the HMD headset will be streamed, and remote input will not be allowed. + /// If this is set to , then the application window will be streamed instead, and remote input will be allowed. + /// The default is unless "VRHeadsetStreaming" "0" is in the extended app info for a game + /// (this is useful for games that have asymmetric multiplayer gameplay). + /// /// public static bool VrHeadsetStreaming { @@ -262,8 +268,42 @@ namespace Steamworks /// - /// Returns whether this steam client is a Steam China specific client, vs the global client + /// Gets whether this steam client is a Steam China specific client (), or the global client (). /// public static bool IsSteamChinaLauncher => Internal != null && Internal.IsSteamChinaLauncher(); + + /// + /// Initializes text filtering, loading dictionaries for the language the game is running in. + /// Users can customize the text filter behavior in their Steam Account preferences. + /// + public static bool InitFilterText() => Internal != null && Internal.InitFilterText( 0 ); + + /// + /// Filters the provided input message and places the filtered result into pchOutFilteredText, + /// using legally required filtering and additional filtering based on the context and user settings. + /// + public static string FilterText( TextFilteringContext context, SteamId sourceSteamID, string inputMessage ) + { + if ( Internal is null ) { return inputMessage; } + Internal.FilterText( context, sourceSteamID, inputMessage, out var filteredString ); + return filteredString; + } + + /// + /// Gets whether or not Steam itself is running on the Steam Deck. + /// + public static bool IsRunningOnSteamDeck => Internal != null && Internal.IsSteamRunningOnSteamDeck(); + + + /// + /// In game launchers that don't have controller support: You can call this to have + /// Steam Input translate the controller input into mouse/kb to navigate the launcher + /// + public static void SetGameLauncherMode( bool mode ) => Internal?.SetGameLauncherMode( mode ); + + //public void ShowFloatingGamepadTextInput( TextInputMode mode, int left, int top, int width, int height ) + //{ + // Internal.ShowFloatingGamepadTextInput( mode, left, top, width, height ); + //} } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamVideo.cs b/Libraries/Facepunch.Steamworks/SteamVideo.cs index f3ec5fabe..745a3f348 100644 --- a/Libraries/Facepunch.Steamworks/SteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/SteamVideo.cs @@ -8,29 +8,28 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Video API. /// public class SteamVideo : SteamClientClass { internal static ISteamVideo? Internal => Interface as ISteamVideo; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamVideo( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); + + return true; } internal static void InstallEvents() { - Dispatch.Install( x => OnBroadcastStarted?.Invoke() ); - Dispatch.Install( x => OnBroadcastStopped?.Invoke( x.Result ) ); } - public static event Action? OnBroadcastStarted; - public static event Action? OnBroadcastStopped; - /// - /// Return true if currently using Steam's live broadcasting + /// Return if currently using Steam's live broadcasting /// public static bool IsBroadcasting { @@ -42,7 +41,7 @@ namespace Steamworks } /// - /// If we're broadcasting, will return the number of live viewers + /// Returns the number of viewers that are watching the stream, or 0 if is . /// public static int NumViewers { @@ -57,4 +56,4 @@ namespace Steamworks } } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs index 963596c7e..6ded4375d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; namespace Steamworks.Data { + /// + /// Represents a Steam Achievement. + /// public struct Achievement { internal string Value; @@ -18,7 +21,7 @@ namespace Steamworks.Data public override string ToString() => Value; /// - /// True if unlocked + /// Gets whether or not the achievement has been unlocked. /// public bool State { @@ -30,15 +33,24 @@ namespace Steamworks.Data } } + /// + /// Gets the identifier of the achievement. This is the "API Name" on Steamworks. + /// public string Identifier => Value; + /// + /// Gets the display name of the achievement. + /// public string? Name => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "name" ); + /// + /// Gets the description of the achievement. + /// public string? Description => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "desc" ); /// - /// Should hold the unlock time if State is true + /// If is , this value represents the time that the achievement was unlocked. /// public DateTime? UnlockTime { @@ -56,7 +68,7 @@ namespace Steamworks.Data /// /// Gets the icon of the achievement. This can return a null image even though the image exists if the image - /// hasn't been downloaded by Steam yet. You can use GetIconAsync if you want to wait for the image to be downloaded. + /// hasn't been downloaded by Steam yet. You should use if you want to wait for the image to be downloaded. /// public Image? GetIcon() { @@ -66,8 +78,9 @@ namespace Steamworks.Data /// - /// Gets the icon of the achievement, waits for it to load if we have to + /// Gets the icon of the achievement, yielding until the icon is received or the is reached. /// + /// The timeout in milliseconds before the request will be canceled. Defaults to 5000. public async Task GetIconAsync( int timeout = 5000 ) { if (SteamUserStats.Internal is null) { return null; } @@ -109,7 +122,7 @@ namespace Steamworks.Data } /// - /// Returns the fraction (0-1) of users who have unlocked the specified achievement, or -1 if no data available. + /// Gets a decimal (0-1) representing the global amount of users who have unlocked the specified achievement, or -1 if no data available. /// public float GlobalUnlocked { @@ -125,7 +138,7 @@ namespace Steamworks.Data } /// - /// Make this achievement earned + /// Unlock this achievement. /// public bool Trigger( bool apply = true ) { @@ -142,7 +155,7 @@ namespace Steamworks.Data } /// - /// Reset this achievement to not achieved + /// Reset this achievement to be locked. /// public bool Clear() { @@ -150,4 +163,4 @@ namespace Steamworks.Data return SteamUserStats.Internal.ClearAchievement( Value ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/AppId.cs b/Libraries/Facepunch.Steamworks/Structs/AppId.cs index 16e4dcd33..eced131ef 100644 --- a/Libraries/Facepunch.Steamworks/Structs/AppId.cs +++ b/Libraries/Facepunch.Steamworks/Structs/AppId.cs @@ -6,6 +6,9 @@ using System.Text; namespace Steamworks { + /// + /// Represents the ID of a Steam application. + /// public struct AppId { public uint Value; @@ -27,4 +30,4 @@ namespace Steamworks return value.Value; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs b/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs index 6d02ff876..2a0654b96 100644 --- a/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs +++ b/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs @@ -6,10 +6,24 @@ using System.Text; namespace Steamworks.Data { + /// + /// Provides information about a DLC. + /// public struct DlcInformation { + /// + /// The of the DLC. + /// public AppId AppId { get; internal set; } + + /// + /// The name of the DLC. + /// public string Name { get; internal set; } + + /// + /// Whether or not the DLC is available. + /// public bool Available { get; internal set; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs b/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs index 1f64d45ef..7a77b3325 100644 --- a/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs +++ b/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs @@ -6,10 +6,29 @@ using System.Text; namespace Steamworks.Data { + /// + /// Represents download progress. + /// public struct DownloadProgress { + /// + /// Whether or not the download is currently active. + /// public bool Active; + + /// + /// How many bytes have been downloaded. + /// public ulong BytesDownloaded; + + /// + /// How many bytes in total the download is. + /// public ulong BytesTotal; + + /// + /// Gets the amount of bytes left that need to be downloaded. + /// + public ulong BytesRemaining => BytesTotal - BytesDownloaded; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs b/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs index 50bd8f79e..d4d21f91a 100644 --- a/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs +++ b/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs @@ -6,10 +6,16 @@ using System.Text; namespace Steamworks.Data { + /// + /// Represents details of a file. + /// public struct FileDetails { + /// + /// The size of the file in bytes. + /// public ulong SizeInBytes; public string Sha1; public uint Flags; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 90a1df26f..abf2593c7 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -261,4 +261,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Image.cs b/Libraries/Facepunch.Steamworks/Structs/Image.cs index 57b00cdcd..8918df6b4 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Image.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Image.cs @@ -7,10 +7,17 @@ namespace Steamworks.Data public uint Height; public byte[] Data; + /// + /// Returns the color of the pixel at the specified position. + /// + /// X-coordinate + /// Y-coordinate + /// The color. + /// If the X and Y or out of bounds. public Color GetPixel( int x, int y ) { - if ( x < 0 || x >= Width ) throw new System.Exception( "x out of bounds" ); - if ( y < 0 || y >= Height ) throw new System.Exception( "y out of bounds" ); + if ( x < 0 || x >= Width ) throw new System.ArgumentException( "x out of bounds" ); + if ( y < 0 || y >= Height ) throw new System.ArgumentException( "y out of bounds" ); Color c = new Color(); @@ -24,14 +31,21 @@ namespace Steamworks.Data return c; } + /// + /// Returns "{Width}x{Height} ({length of }bytes)" + /// + /// public override string ToString() { return $"{Width}x{Height} ({Data.Length}bytes)"; } } + /// + /// Represents a color. + /// public struct Color { public byte r, g, b, a; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 397e82350..c16a390ec 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -5,6 +5,9 @@ using System.Threading.Tasks; namespace Steamworks.Data { + /// + /// Represents a Steam lobby. + /// public struct Lobby { public SteamId Id { get; internal set; } @@ -18,8 +21,8 @@ namespace Steamworks.Data } /// - /// Try to join this room. Will return RoomEnter.Success on success, - /// and anything else is a failure + /// Try to join this room. Will return on success, + /// and anything else is a failure. /// public async Task Join() { @@ -41,9 +44,9 @@ namespace Steamworks.Data } /// - /// Invite another user to the lobby - /// will return true if the invite is successfully sent, whether or not the target responds - /// returns false if the local user is not connected to the Steam servers + /// Invite another user to the lobby. + /// Will return if the invite is successfully sent, whether or not the target responds + /// returns if the local user is not connected to the Steam servers /// public bool InviteFriend( SteamId steamid ) { @@ -51,12 +54,12 @@ namespace Steamworks.Data } /// - /// returns the number of users in the specified lobby + /// Gets the number of users in this lobby. /// public int MemberCount => SteamMatchmaking.Internal?.GetNumLobbyMembers( Id ) ?? 0; /// - /// Returns current members. Need to be in the lobby to see the users. + /// Returns current members in the lobby. The current user must be in the lobby in order to see the users. /// public IEnumerable Members { @@ -72,7 +75,7 @@ namespace Steamworks.Data /// - /// Get data associated with this lobby + /// Get data associated with this lobby. /// public string? GetData( string key ) { @@ -80,7 +83,7 @@ namespace Steamworks.Data } /// - /// Get data associated with this lobby + /// Set data associated with this lobby. /// public bool SetData( string key, string value ) { @@ -91,7 +94,7 @@ namespace Steamworks.Data } /// - /// Removes a metadata key from the lobby + /// Removes a metadata key from the lobby. /// public bool DeleteData( string key ) { @@ -99,7 +102,7 @@ namespace Steamworks.Data } /// - /// Get all data for this lobby + /// Get all data for this lobby. /// public IEnumerable> Data { @@ -119,7 +122,7 @@ namespace Steamworks.Data } /// - /// Gets per-user metadata for someone in this lobby + /// Gets per-user metadata for someone in this lobby. /// public string? GetMemberData( Friend member, string key ) { @@ -127,7 +130,7 @@ namespace Steamworks.Data } /// - /// Sets per-user metadata (for the local user implicitly) + /// Sets per-user metadata (for the local user implicitly). /// public void SetMemberData( string key, string value ) { @@ -135,35 +138,44 @@ namespace Steamworks.Data } /// - /// Sends a string to the chat room + /// Sends a string to the chat room. /// public bool SendChatString( string message ) { - var data = System.Text.Encoding.UTF8.GetBytes( message ); + //adding null terminator as it's used in Helpers.MemoryToString + var data = System.Text.Encoding.UTF8.GetBytes( message + '\0' ); return SendChatBytes( data ); } /// - /// Sends bytes the the chat room - /// this isn't exposed because there's no way to read raw bytes atm, - /// and I figure people can send json if they want something more advanced + /// Sends bytes to the chat room. /// - internal unsafe bool SendChatBytes( byte[] data ) + public unsafe bool SendChatBytes( byte[] data ) { fixed ( byte* ptr = data ) { - return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); + return SendChatBytesUnsafe( ptr, data.Length ); } } + /// + /// Sends bytes to the chat room from an unsafe buffer. + /// + public unsafe bool SendChatBytesUnsafe( byte* ptr, int length ) + { + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, length ); + } + /// /// Refreshes metadata for a lobby you're not necessarily in right now. + /// /// You never do this for lobbies you're a member of, only if your /// this will send down all the metadata associated with a lobby. /// This is an asynchronous call. - /// Returns false if the local user is not connected to the Steam servers. + /// Returns if the local user is not connected to the Steam servers. /// Results will be returned by a LobbyDataUpdate_t callback. - /// If the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false. + /// If the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to . + /// /// public bool Refresh() { @@ -171,8 +183,8 @@ namespace Steamworks.Data } /// - /// Max members able to join this lobby. Cannot be over 250. - /// Can only be set by the owner + /// Max members able to join this lobby. Cannot be over 250. + /// Can only be set by the owner of the lobby. /// public int MaxMembers { @@ -180,26 +192,42 @@ namespace Steamworks.Data set => SteamMatchmaking.Internal?.SetLobbyMemberLimit( Id, value ); } + /// + /// Sets the lobby as public. + /// public bool SetPublic() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); } + /// + /// Sets the lobby as private. + /// public bool SetPrivate() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); } + /// + /// Sets the lobby as invisible. + /// public bool SetInvisible() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); } + /// + /// Sets the lobby as friends only. + /// public bool SetFriendsOnly() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); } + /// + /// Set whether or not the lobby can be joined. + /// + /// Whether or not the lobby can be joined. public bool SetJoinable( bool b ) { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); @@ -207,7 +235,7 @@ namespace Steamworks.Data /// /// [SteamID variant] - /// Allows the owner to set the game server associated with the lobby. Triggers the + /// Allows the owner to set the game server associated with the lobby. Triggers the /// Steammatchmaking.OnLobbyGameCreated event. /// public void SetGameServer( SteamId steamServer ) @@ -220,7 +248,7 @@ namespace Steamworks.Data /// /// [IP/Port variant] - /// Allows the owner to set the game server associated with the lobby. Triggers the + /// Allows the owner to set the game server associated with the lobby. Triggers the /// Steammatchmaking.OnLobbyGameCreated event. /// public void SetGameServer( string ip, ushort port ) @@ -232,7 +260,7 @@ namespace Steamworks.Data } /// - /// Gets the details of the lobby's game server, if set. Returns true if the lobby is + /// Gets the details of the lobby's game server, if set. Returns true if the lobby is /// valid and has a server set, otherwise returns false. /// public bool GetGameServer( ref uint ip, ref ushort port, ref SteamId serverId ) @@ -241,7 +269,7 @@ namespace Steamworks.Data } /// - /// You must be the lobby owner to set the owner + /// Gets or sets the owner of the lobby. You must be the lobby owner to set the owner /// public Friend Owner { @@ -250,8 +278,8 @@ namespace Steamworks.Data } /// - /// Check if the specified SteamId owns the lobby + /// Check if the specified SteamId owns the lobby. /// public bool IsOwnedBy( SteamId k ) => Owner.Id == k; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs index 5254c9f50..b3907eb71 100644 --- a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs +++ b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs @@ -10,7 +10,7 @@ namespace Steamworks internal PartyBeaconID_t Id; /// - /// Creator of the beacon + /// Gets the owner of the beacon. /// public SteamId Owner { @@ -24,7 +24,7 @@ namespace Steamworks } /// - /// Creator of the beacon + /// Gets metadata related to the beacon. /// public string? MetaData { @@ -40,7 +40,7 @@ namespace Steamworks /// /// Will attempt to join the party. If successful will return a connection string. - /// If failed, will return null + /// If failed, will return /// public async Task JoinAsync() { @@ -55,7 +55,7 @@ namespace Steamworks /// /// When a user follows your beacon, Steam will reserve one of the open party slots for them, and send your game a ReservationNotification callback. - /// When that user joins your party, call OnReservationCompleted to notify Steam that the user has joined successfully + /// When that user joins your party, call this method to notify Steam that the user has joined successfully. /// public void OnReservationCompleted( SteamId steamid ) { @@ -73,11 +73,11 @@ namespace Steamworks } /// - /// Turn off the beacon + /// Turn off the beacon. /// public bool Destroy() { return Internal != null && Internal.DestroyBeacon( Id ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs index a30b506e4..3d95000d7 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs @@ -6,6 +6,9 @@ using System.Text; namespace Steamworks.Data { + /// + /// Represents a screenshot that was taken by a user. + /// public struct Screenshot { internal ScreenshotHandle Value; @@ -19,19 +22,16 @@ namespace Steamworks.Data } /// - /// Tags a user as being visible in the screenshot + /// Sets the location of the screenshot. /// public bool SetLocation( string location ) { return SteamScreenshots.Internal != null && SteamScreenshots.Internal.SetLocation( Value, location ); } - /// - /// Tags a user as being visible in the screenshot - /// public bool TagPublishedFile( PublishedFileId file ) { return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagPublishedFile( Value, file ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Server.cs b/Libraries/Facepunch.Steamworks/Structs/Server.cs index 5e74b0a35..b3d2abd9e 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Server.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Server.cs @@ -142,4 +142,4 @@ namespace Steamworks.Data return (Address?.GetHashCode() ?? 0) + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs index 637c85c06..95e698271 100644 --- a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs +++ b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs @@ -21,8 +21,7 @@ namespace Steamworks /// public struct SteamServerInit { - public IPAddress? IpAddress; - public ushort SteamPort; + public IPAddress? IpAddress; public ushort GamePort; public ushort QueryPort; public InitServerMode Mode; @@ -60,18 +59,8 @@ namespace Steamworks Mode = InitServerMode.Authentication; VersionString = "1.0.0.0"; IpAddress = null; - SteamPort = 0; } - /// - /// Set the Steam quert port - /// - public SteamServerInit WithRandomSteamPort() - { - SteamPort = (ushort)new Random().Next( 10000, 60000 ); - return this; - } - /// /// If you pass MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE into usQueryPort, then it causes the game server API to use /// "GameSocketShare" mode, which means that the game is responsible for sending and receiving UDP packets for the master diff --git a/Libraries/Facepunch.Steamworks/Structs/Stat.cs b/Libraries/Facepunch.Steamworks/Structs/Stat.cs index 505ffeb4b..99660ed1a 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Stat.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Stat.cs @@ -151,4 +151,4 @@ namespace Steamworks.Data return SteamUserStats.Internal != null && SteamUserStats.Internal.StoreStats(); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/SteamId.cs b/Libraries/Facepunch.Steamworks/Structs/SteamId.cs index 48c0a0c4e..9977e5fd6 100644 --- a/Libraries/Facepunch.Steamworks/Structs/SteamId.cs +++ b/Libraries/Facepunch.Steamworks/Structs/SteamId.cs @@ -6,6 +6,9 @@ using System.Text; namespace Steamworks { + /// + /// Represents the ID of a user or steam lobby. + /// public struct SteamId { public ulong Value; @@ -26,4 +29,4 @@ namespace Steamworks public bool IsValid => Value != default; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs new file mode 100644 index 000000000..8b95d3398 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Steamworks.Data +{ + public struct UgcAdditionalPreview + { + internal UgcAdditionalPreview( string urlOrVideoID, string originalFileName, ItemPreviewType itemPreviewType ) + { + this.UrlOrVideoID = urlOrVideoID; + this.OriginalFileName = originalFileName; + this.ItemPreviewType = itemPreviewType; + } + + public string UrlOrVideoID { get; private set; } + public string OriginalFileName { get; private set; } + public ItemPreviewType ItemPreviewType { get; private set; } + } +} diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta new file mode 100644 index 000000000..511f7d5ad --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 48f086235d5dbeb44bccbb40802e30fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index 1067141b5..6a8403853 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -44,7 +44,12 @@ namespace Steamworks.Ugc /// Workshop item that is meant to be voted on for the purpose of selling in-game /// public static Editor NewMicrotransactionFile => new Editor( WorkshopFileType.Microtransaction ); - + + /// + /// Workshop item that is meant to be managed by the game. It is queryable by the API, but isn't visible on the web browser. + /// + public static Editor NewGameManagedFile => new Editor(WorkshopFileType.GameManagedItem); + public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; } public string? Title { get; private set; } @@ -136,14 +141,7 @@ namespace Steamworks.Ugc return this; } - public bool HasTag( string tag ) - { - if (Tags != null && Tags.Contains(tag)) { return true; } - - return false; - } - - public async Task SubmitAsync( IProgress? progress = null ) + public async Task SubmitAsync( IProgress? progress = null, Action? onItemCreated = null ) { var result = default( PublishResult ); if (SteamUGC.Internal is null) { return result; } @@ -184,6 +182,9 @@ namespace Steamworks.Ugc FileId = created.Value.PublishedFileId; result.NeedsWorkshopAgreement = created.Value.UserNeedsToAcceptWorkshopLegalAgreement; result.FileId = FileId; + + if ( onItemCreated != null ) + onItemCreated( result ); } result.FileId = FileId; @@ -260,7 +261,7 @@ namespace Steamworks.Ugc case ItemUpdateStatus.UploadingContent: { var uploaded = total > 0 ? ((float)processed / (float)total) : 0.0f; - progress?.Report( 0.2f + uploaded * 0.7f ); + progress?.Report( 0.2f + uploaded * 0.6f ); break; } case ItemUpdateStatus.UploadingPreviewFile: @@ -311,4 +312,4 @@ namespace Steamworks.Ugc /// public bool NeedsWorkshopAgreement; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 49ff42292..ebfb6b8ec 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -116,6 +116,15 @@ namespace Steamworks.Ugc /// The number of downvotes of this item ///
public uint VotesDown => details.VotesDown; + /// + /// Dependencies/children of this item or collection, available only from WithDependencies(true) queries + /// + public PublishedFileId[]? Children; + + /// + /// Additional previews of this item or collection, available only from WithAdditionalPreviews(true) queries + /// + public UgcAdditionalPreview[]? AdditionalPreviews { get; internal set; } public bool IsInstalled => (State & ItemState.Installed) == ItemState.Installed; public bool IsDownloading => (State & ItemState.Downloading) == ItemState.Downloading; @@ -410,7 +419,21 @@ namespace Steamworks.Ugc { return new Ugc.Editor( Id ); } - + + public async Task AddDependency( PublishedFileId child ) + { + if ( SteamUGC.Internal is null ) { return false; } + var r = await SteamUGC.Internal.AddDependency( Id, child ); + return r?.Result == Result.OK; + } + + public async Task RemoveDependency( PublishedFileId child ) + { + if ( SteamUGC.Internal is null ) { return false; } + var r = await SteamUGC.Internal.RemoveDependency( Id, child ); + return r?.Result == Result.OK; + } + public Result Result => details.Result; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index cfb65ca75..b877e0096 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -154,6 +154,8 @@ namespace Steamworks.Ugc ReturnsKeyValueTags = WantsReturnKeyValueTags ?? false, ReturnsDefaultStats = WantsDefaultStats ?? true, //true by default ReturnsMetadata = WantsReturnMetadata ?? false, + ReturnsChildren = WantsReturnChildren ?? false, + ReturnsAdditionalPreviews = WantsReturnAdditionalPreviews ?? false, }; } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs index 54ffe6973..be4340580 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs @@ -15,6 +15,8 @@ namespace Steamworks.Ugc internal bool ReturnsKeyValueTags; internal bool ReturnsDefaultStats; internal bool ReturnsMetadata; + internal bool ReturnsChildren; + internal bool ReturnsAdditionalPreviews; public IEnumerable Entries { @@ -74,8 +76,36 @@ namespace Steamworks.Ugc } } - // TODO GetQueryUGCAdditionalPreview - // TODO GetQueryUGCChildren + uint numChildren = item.details.NumChildren; + if ( ReturnsChildren && numChildren > 0 ) + { + var children = new PublishedFileId[numChildren]; + if ( SteamUGC.Internal.GetQueryUGCChildren( Handle, i, children, numChildren ) ) + { + item.Children = children; + } + } + + if ( ReturnsAdditionalPreviews ) + { + var previewsCount = SteamUGC.Internal.GetQueryUGCNumAdditionalPreviews( Handle, i ); + if ( previewsCount > 0 ) + { + item.AdditionalPreviews = new UgcAdditionalPreview[previewsCount]; + for ( uint j = 0; j < previewsCount; j++ ) + { + string previewUrlOrVideo; + string originalFileName; //what is this??? + ItemPreviewType previewType = default; + if ( SteamUGC.Internal.GetQueryUGCAdditionalPreview( + Handle, i, j, out previewUrlOrVideo, out originalFileName, ref previewType ) ) + { + item.AdditionalPreviews[j] = new UgcAdditionalPreview( + previewUrlOrVideo, originalFileName, previewType ); + } + } + } + } yield return item; } diff --git a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs index 968e9d0ea..644d1a31f 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs @@ -12,38 +12,49 @@ namespace Steamworks public const int MemoryBufferSize = 1024 * 32; internal struct Memory : IDisposable - { + { private const int MaxBagSize = 4; - private static readonly ConcurrentBag BufferBag = new ConcurrentBag(); + private static readonly Queue BufferBag = new Queue(); public IntPtr Ptr { get; private set; } public static implicit operator IntPtr(in Memory m) => m.Ptr; - internal unsafe Memory(int sz) + internal static unsafe Memory Take() { - Ptr = BufferBag.TryTake(out IntPtr ptr) ? ptr : Marshal.AllocHGlobal(sz); - ((byte*)Ptr)[0] = 0; + IntPtr ptr; + lock (BufferBag) + { + ptr = BufferBag.Count > 0 ? BufferBag.Dequeue() : Marshal.AllocHGlobal(MemoryBufferSize); + } + ((byte*)ptr)[0] = 0; + return new Memory + { + Ptr = ptr + }; } public void Dispose() { if (Ptr == IntPtr.Zero) { return; } - if (BufferBag.Count < MaxBagSize) + lock (BufferBag) { - BufferBag.Add(Ptr); + if (BufferBag.Count < MaxBagSize) + { + BufferBag.Enqueue(Ptr); + } + else + { + Marshal.FreeHGlobal(Ptr); + } } - else - { - Marshal.FreeHGlobal(Ptr); - } Ptr = IntPtr.Zero; } - } - + } + public static Memory TakeMemory() { - return new Memory(MemoryBufferSize); + return Memory.Take(); } private static byte[][] BufferPool = new byte[4][]; diff --git a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs index ca0b5a20c..c2c30b490 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs @@ -55,7 +55,7 @@ namespace Steamworks public abstract class SteamClass { - internal abstract void InitializeInterface( bool server ); + internal abstract bool InitializeInterface( bool server ); internal abstract void DestroyInterface( bool server ); } @@ -65,9 +65,9 @@ namespace Steamworks internal static SteamInterface? InterfaceClient; internal static SteamInterface? InterfaceServer; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { - + return false; } internal virtual void SetInterface( bool server, SteamInterface iface ) @@ -101,9 +101,9 @@ namespace Steamworks { internal static SteamInterface? Interface; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { - + return false; } internal virtual void SetInterface( bool server, SteamInterface iface ) @@ -124,9 +124,9 @@ namespace Steamworks { internal static SteamInterface? Interface; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { - + return false; } internal virtual void SetInterface( bool server, SteamInterface iface ) diff --git a/Libraries/Facepunch.Steamworks/libsteam_api64.dylib b/Libraries/Facepunch.Steamworks/libsteam_api64.dylib new file mode 100644 index 0000000000000000000000000000000000000000..5237be0b29f03ea83a72332d19cbc9030556b680 GIT binary patch literal 610496 zcmeEv3w%_?_5bWLq7kA}STzV#3JFprikdeG64*QlsK64E4GD%MZgzQC(Z!I* z<+8j4d_P*XYSl_r9<_=I&;-ybK5DVpii*0Jw2i2UO4-_0j? z?mW(%IdkUBnKN_mp1belb2}JgF}V7OdkSO5paby%1~o5+@?PDq zfJ^_=IiPbu=YY-uodY@tbPnho&^e%UK<9wY0i6Rn2XqeT9MCzSb3o^S&Hs7WX9Is!x-I7DiGaG z%%>)u&f295=T%oaYdzk1$c)C}_?R&VNT9n)3Dq_C5;&+Yz=6una#v-I%iRTz2Lhsh zbVoPCe+mxDa5_tBJ?@IC`ATUt9nXHk*b`tH-L-#Od`{>5k`kwDNr|h*Cfq`!dQwSt?{^#KDGV?4rz#Jo5oS(6OQ@SVGd9_ z62}Cz4e|khDvm6Uo9a{+pN@`+sCx*obl38L`Y|blBYG?%=@?=#ur~oqcP$R3I=?xc zrNy4&a87hzI#JgIuN-v$b{r+u3l~;bsaT?Md~PzZL}Cd3v^X$Ap+Bc{UTtkCbt+F2 z`WjfgQsLWhP%({>(>Wz)>eZPUIfV+s6qGdM!rk$r&=U$O4$xnz7mvHhr_jJsDyxf2 zU2bPhv8Q}&WyQQ&kE?j0v$&>WOzBc2+4-0*iG%@osoe>e{QVnb-rPB;-H2Phd z1s+~6`c7?(+-hJol>avaTh+{1Kje|46Z#V^GxigdkHM8Dvbzk++YRiNE=tZt9<}pS zM#ks&9=PJQZwJYIsBfsOUnx~czk zm0Wbum?WkD1V26k{smAR316@jTw6FMO{u~z1U$7_{u={p2J|Q;O+2B$A1Ue1Cyi`w z9?#MPodY@tbPnho&^e%UK<9wY0i6Rn2XqeT9MCzSb3o^S&HUP_thC$CELItr8@PZfA zf^*b@^F+ZzYQazx%yG`%wHDZm931<#D|2nK*CJ<{ea}6=6-0P!bsZ+_*1dTE7=d*A zfN=Ulq|r1Q4KA_-7nuV_jyY})r?+56QC){c_L}`So8&B0eY58RIoBfRn51u#UQf=p zS?}3t%{?HS*48(B|DODfr`P3I+m?-&Znm^#b^eoDz?kXUWq$eeWAkU$P`v zx9*dsB$$Hx>vmh_IIo+%OX{p^wMa8V71M11ygb$9eO~%BFb585^lV@NYc3E@3lqcR_AH+Dk;<+rj$ac$5lbUh!j*;h> z>JNHirFZ3X>koR5w-1tUHYZ=|vDUSk7@4$d^AGb8luQ+Lm5Q4 zzS+8>f#hY!0;HH{nQzCY-y!1lki;#s$kWZO4U}V$ZjAHKK4jhc6$H(n+nHCOFvzx| z*>j1Uc?cbmQb{Uj6yycc{uvCSzj6a%g`o6<$f@wM(y(_cM5!$$4y`fK2BdaKB&`;Z zuyUH|Sc}2B;z7XC1bFXc(=?OpFx9_qU9m>w`Lj*3(+tKnCtnN3HCuC!%5k^Wzs?y~ zb@?3IO%?Javy|B(XLclK*}T^!-yG+;So->OZ0s#BF(>!&*MN=0Md?r8o$VLM%go7( zEtosgEP;FAs7vy8(hz5MV#bpM(6m#ee{JNSDyxF~Gv(Ykd6^9Zj){0I%Wm+__D>xm zEgI>+V?DYg%^BU6MShwX=2crpg6Ty#YYIFK&r1HA#~;g@VDOlwi3x!Zks&Qgk>+Hy zWo2{diDBp!D5O@<#4?*dGayZnvQ3i1T-Q290!C?kLLe6Iom7@b(I;gd2CrK)KVx!= zgM{QKaNa~3DA(-In<7m=<+mmF4=9m=B#$|G?3><;US zFF@Dj1-5g47H7Tn2;xb{FzZ3o#c3w_YTL!JeOs*cEy$PC<7Kn#FiX?lfvh-!d1mmk z{+RbNsg2SB(;b|1VoOw97*0F`)?DfYy3#2cB z)VekIGZ|tDMne~xf|uk;CucZ<`6d9#>7@Q*r8XHd%4gBf%*n$%R)4x#W@7kLF#OW& z03YN48a)(P2VY(w?eb+E(l^%nvB6-l-IR2!{TZ}~1;B#U;C2q6GB!N#Xol>Oa~$=1 ztt+0VE-#CdXWRS^u=8+Tvx8JaUR?4uwzI#suHR|h`nol}MYgP~-wRG=c3M|R#L1a9 z>8o?IUUSXQR5;lr<)3Ix8-iGZG}$gq z9xY8C>Ax@eDaN*&(Rafx>DqYzeUBlH=;Vpg4b4ARfcVt&muK~DUuomik9d4XS}qJ~YXo#5MXE6NSt zbD>KDFPwy)B;(+G?e&Z`n|gK?wdV*7;Gj08XD~tHp!otX07~*Cm;%onBTQtXsA^NCLRqc?>j5gS-nJRy zv)6MKn!swrbwQ`2U`FsYh$h5tMBsV?Yxo_3HGBGELe8L7+!iR%RQI-Nh@O4K3jweW zgOO&DGZJhh3V(qT4*VJch$wPE%YasSszolc#Y~Mmw$tG0Ehou^z;rJnvbQJNWlZzbe@3T zMTuD?lA6CB(l+~uUyY_2~?0%&GLlgS3MS(mZN>QSeVD!o**wVAvv6q z-eGlZmZobg$(_~>DVC;T(oXM-M|PoN@<7i7X4nra4K zDUCvysb=fOT~gc~Qmm9`1~c1dg56TmX#k<*Ib>^^#n75!VYF}!l*pL##1K|+0ObzC zZ6w^5yCg&V7-?S!-VACau>4!WQ%zDf*o6HD%|RhQn3ho4(@fADv7yy)ki6KOd?i-< zX%VYl@&vC@N(^EtOfg9}@f}EDAh1Nsq1jzP>0QE`k^z@OOCh67oGo)cK_05nkOh4Ea z&C2?qqn~0*mw$%8$;q)DcBlhg+S`7Xo;B9r_E8pQNYq-ieDcT#&?rW67q+Vh;_TY||DCuf_n z;Wbz{?37LGjs&5Eq~p~5k@gU|y$PQuDI|Q5UNdAUM#9v}HcOK!1QQH=7ThVN@fqCO zZj^RnGOWzDG)=br#?XX2Mxr}{egn-M-ezK`%~+T-r)kqjO5N`%u+Pq2!|CoPZc$roXUKGt)JTE~4O*vda0M z$9l~fr)E4AopD!mhF{GPyyg|1(DMt7Fy{hyp;_WgEA}Jve9Itwv*i1a{Q{XVr5aE8 z84F(Q2;9M;%)yJPiYyZT#&e;ZsPsmGBalsZoB<(gz6vu+fhiI&;U$TB1&Wq;Of4789|;q;Cv>QDj+E1ET#hYV;2TvxdX;MdNW~ zk@y>{NS_0462YAz@)DK2i>Vt2VGQ|RpdK=Bg1_+@Dx*z!UQZu?wjucy??+Jlt?-x~?zev%F%~8j^`0XvB7_YT zrR>1vNY))U-gJ(C!-Tm6`WmJBX8ZGkzT~mq&Q`+4Igakw?Zi169oaxz$NI+!%Yo&) zX>j?{sO^AhEHQIu6StUiQv7}*c~r~t78-?TaPV@R?6cdw#NRWdjU7On^g7lt9$OhV zt|=~Y#p~!=ks^>t!=1W%nBc@flv(>J@(L485!Tsg=6dYh7O(~7j)K5X&*c*dLtP4(f`DrXPfM+v z4Rt5yFV5rhiQoadMU-tX2OkzNhromUPtOr`cFB#;;!xMWMh1M*a|D*37fcNF10^(B z540PTnl-i^pGUJGy_~d}vhOU^mg;xY4uoMgS!Xo+x7>l0byRQbs4QDsmdOsBAnA7_ zQOp0r9+VZ#2waVWIl}KWwPnQ*;1HIiW=DC`nb=Vk(cFI*l*10hQHHb&%k8;xrVZvt zoZlY+25gQTdHI3(Fh!`SNA=zO*1Y2T$_}8 zJkL>1;!qUWFNjh^vu?L3WH&GG7-ClXd@Y%^E6=+R)RFqAKW1Gq90M--50UU3Y`Sas zgGk-=*dr7^$;01JI21hCPVGn=Y2N!^AYp?AoeGk$PvJ$58S)EM7?=z(1$&z6+mQmH z6XHDu^8LwZ6g`I+7=jlEHi%_H>P>p$G0s9vd(x2pEgh&2l&9gmNkM3P)0~0{o5ns~9VA1mB`DYdjl0q)Ls& z$aMs_Z9)G6EuyB>_*{0IEyLq;jMg1i23aIJ-J3%_Hi~4IY&$;nj=pL zw5<&>NTMs+IkF1ECh*9c|9KY%qBkZ8r00drG%tq*-b7Op&Z?aj$!Q}?n%SprgI90mB&JwC9o172f(@u6ybGuuy)gSLf`h$0Ae1qfAeN~ ztkm1T`61l7lyG!k`@kG7At6jjduJa(ozS`9XionedB;U}qGlhmn0CAb+|1Kwv*F12 z7_{{g4jPfu6z^YsCd3D}hJ~+)4KETU++VPzxdL*Ng$P|rR6$u7(Eq2=R2?^u84fmV)` zk4PCL6gh%_N>lr)e7dR zA`^J#6D)(}5Y4(B!=TOCE$6DEBX1f3NmN86xULC>N)eX`c@xQr9Pi(H15xk6SQUZ` z%JbHOI2xC9C;(-g%nG8w>|R);eoO@oV@cA5QP*%k1iC$~ey^JA87+q9d=hjFLSR`` z^ES1qd=t!W$cMk7Oo4qKuz@;E1*l^yDPlYa8ZESm6GhM%NqbXgAGsC#G{n`}pR_kn zgPgh@k3pp}oQl}@geEJWo3O$fQKas2mv_vCQ3(MWQ;ta@N_|U~?`ZM=aueaVr z6|MEVK#{+34^}AN)5dod#I3wsdZGDa=<^j%14?Qo1^>bx${5wyNj(|Wcu3s#i(9j} zZ5Fo�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

eK!jPOTWAaB<}T4a?Yr1HiGC0bxJg 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

`BGjO;44V~u%b*hwdH^ZTZV{Jt^d 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
  • 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 literal 262944 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 diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs index f72a4e7d6..c79e46edd 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs @@ -249,12 +249,12 @@ namespace FarseerPhysics.Dynamics.Contacts /// The contact manager. internal void Update(ContactManager contactManager) { - Body bodyA = FixtureA.Body; - Body bodyB = FixtureB.Body; - if (FixtureA == null || FixtureB == null) return; + Body bodyA = FixtureA.Body; + Body bodyB = FixtureB.Body; + Manifold oldManifold = Manifold; // Re-enable this contact. diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index bddd50da5..dd220d533 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -69,15 +69,8 @@ namespace Lidgren.Network return; // remove all callbacks regardless of sync context - RestartRemoveCallbacks: - for (int i = 0; i < m_receiveCallbacks.Count; i++) - { - if (m_receiveCallbacks[i].Item2.Equals(callback)) - { - m_receiveCallbacks.RemoveAt(i); - goto RestartRemoveCallbacks; - } - } + m_receiveCallbacks.RemoveAll(tuple => tuple.Item2.Equals(callback)); + if (m_receiveCallbacks.Count < 1) m_receiveCallbacks = null; } @@ -123,49 +116,55 @@ namespace Lidgren.Network } m_lastSocketBind = now; - if (m_socket == null) + using (var mutex = new Mutex(false, "Global\\lidgrenSocketBind")) { try { - m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + mutex.WaitOne(); + + if (m_socket == null) + m_socket = new Socket(m_configuration.LocalAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + + if (reBind) + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); + + m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; + m_socket.SendBufferSize = m_configuration.SendBufferSize; + m_socket.Blocking = false; + + if (m_configuration.DualStack) + { + if (m_configuration.LocalAddress.AddressFamily != AddressFamily.InterNetworkV6) + { + LogWarning("Configuration specifies Dual Stack but does not use IPv6 local address; Dual stack will not work."); + } + else + { + m_socket.DualMode = true; + } + } + + var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress, reBind ? m_listenPort : m_configuration.Port); + m_socket.Bind(ep); + + try + { + const uint IOC_IN = 0x80000000; + const uint IOC_VENDOR = 0x18000000; + uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); + } + catch + { + // ignore; SIO_UDP_CONNRESET not supported on this platform + } } - catch (SocketException socketException) + finally { - if (socketException.SocketErrorCode == SocketError.AddressFamilyNotSupported) - { - // Fall back to IPv4 if IPv6 is unsupported - m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - } - else - { - throw; - } + mutex.ReleaseMutex(); } } - if (reBind) - m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); - - m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; - m_socket.SendBufferSize = m_configuration.SendBufferSize; - m_socket.Blocking = false; - if (m_socket.AddressFamily == AddressFamily.InterNetworkV6) { m_socket.DualMode = m_configuration.UseDualModeSockets; } - - var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToFamily(m_socket.AddressFamily), reBind ? m_listenPort : m_configuration.Port); - m_socket.Bind(ep); - - try - { - const uint IOC_IN = 0x80000000; - const uint IOC_VENDOR = 0x18000000; - uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; - m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); - } - catch - { - // ignore; SIO_UDP_CONNRESET not supported on this platform - } - var boundEp = m_socket.LocalEndPoint as NetEndPoint; LogDebug("Socket bound to " + boundEp + ": " + m_socket.IsBound); m_listenPort = boundEp.Port; @@ -269,7 +268,7 @@ namespace Lidgren.Network Heartbeat(); NetUtility.Sleep(10); - + lock (m_initializeLock) { try @@ -425,176 +424,175 @@ namespace Lidgren.Network // update now now = NetTime.Now; - do + try + { + do + { + ReceiveSocketData(now); + } while (m_socket.Available > 0); + } + catch (SocketException sx) + { + switch (sx.SocketErrorCode) + { + case SocketError.ConnectionReset: + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! + // So, what to do? + LogWarning("ConnectionReset"); + return; + + case SocketError.NotConnected: + // socket is unbound; try to rebind it (happens on mobile when process goes to sleep) + BindSocket(true); + return; + + default: + LogWarning("Socket exception: " + sx.ToString()); + return; + } + } + } + + private void ReceiveSocketData(double now) + { + int bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); + + if (bytesReceived < NetConstants.HeaderByteSize) + return; + + //LogVerbose("Received " + bytesReceived + " bytes"); + + var ipsender = (NetEndPoint)m_senderRemote; + + if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline && bytesReceived > 32) { - int bytesReceived = 0; - try + // is this an UPnP response? + string resp = System.Text.Encoding.UTF8.GetString(m_receiveBuffer, 0, bytesReceived); + if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) { - if (m_senderRemote is IPEndPoint ipEndpoint && ipEndpoint.AddressFamily != m_socket.AddressFamily) - { - m_senderRemote = ipEndpoint.MapToFamily(m_socket.AddressFamily); - } - bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); - } - catch (SocketException sx) - { - switch (sx.SocketErrorCode) - { - case SocketError.ConnectionReset: - // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" - // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! - // So, what to do? - LogWarning("ConnectionReset"); - return; - - case SocketError.NotConnected: - // socket is unbound; try to rebind it (happens on mobile when process goes to sleep) - BindSocket(true); - return; - - default: - LogWarning("Socket exception: " + sx.ToString()); - return; - } - } - - if (bytesReceived < NetConstants.HeaderByteSize) - return; - - //LogVerbose("Received " + bytesReceived + " bytes"); - - var ipsender = (NetEndPoint)m_senderRemote; - - if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline && bytesReceived > 32) - { - // is this an UPnP response? - string resp = System.Text.Encoding.UTF8.GetString(m_receiveBuffer, 0, bytesReceived); - if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) - { - try - { - resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); - resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); - m_upnp.ExtractServiceUrl(resp); - return; - } - catch (Exception ex) - { - LogDebug("Failed to parse UPnP response: " + ex.ToString()); - - // don't try to parse this packet further - return; - } - } - } - - NetConnection sender = null; - m_connectionLookup.TryGetValue(ipsender, out sender); - - // - // parse packet into messages - // - int numMessages = 0; - int numFragments = 0; - int ptr = 0; - while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) - { - // decode header - // 8 bits - NetMessageType - // 1 bit - Fragment? - // 15 bits - Sequence number - // 16 bits - Payload length in bits - - numMessages++; - - NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; - - byte low = m_receiveBuffer[ptr++]; - byte high = m_receiveBuffer[ptr++]; - - bool isFragment = ((low & 1) == 1); - ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); - - if (isFragment) - numFragments++; - - ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); - int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); - - if (bytesReceived - ptr < payloadByteLength) - { - LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); - return; - } - - if (tp >= NetMessageType.Unused1 && tp <= NetMessageType.Unused29) - { - ThrowOrLog("Unexpected NetMessageType: " + tp); - return; - } - try { - if (tp >= NetMessageType.LibraryError) - { - if (sender != null) - sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); - else - ReceivedUnconnectedLibraryMessage(now, ipsender, tp, ptr, payloadByteLength); - } - else - { - if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) - return; // dropping unconnected message since it's not enabled - - NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); - msg.m_isFragment = isFragment; - msg.m_receiveTime = now; - msg.m_sequenceNumber = sequenceNumber; - msg.m_receivedMessageType = tp; - msg.m_senderConnection = sender; - msg.m_senderEndPoint = ipsender; - msg.m_bitLength = payloadBitLength; - - Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); - if (sender != null) - { - if (tp == NetMessageType.Unconnected) - { - // We're connected; but we can still send unconnected messages to this peer - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } - else - { - // connected application (non-library) message - sender.ReceivedMessage(msg); - } - } - else - { - // at this point we know the message type is enabled - // unconnected application (non-library) message - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } - } + resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); + resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); + m_upnp.ExtractServiceUrl(resp); + return; } catch (Exception ex) { - LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + LogDebug("Failed to parse UPnP response: " + ex.ToString()); + + // don't try to parse this packet further + return; } - ptr += payloadByteLength; + } + } + + NetConnection sender = null; + m_connectionLookup.TryGetValue(ipsender, out sender); + + // + // parse packet into messages + // + int numMessages = 0; + int numFragments = 0; + int ptr = 0; + while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) + { + // decode header + // 8 bits - NetMessageType + // 1 bit - Fragment? + // 15 bits - Sequence number + // 16 bits - Payload length in bits + + numMessages++; + + NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; + + byte low = m_receiveBuffer[ptr++]; + byte high = m_receiveBuffer[ptr++]; + + bool isFragment = ((low & 1) == 1); + ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); + + if (isFragment) + numFragments++; + + ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); + int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); + + if (bytesReceived - ptr < payloadByteLength) + { + LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); + return; } - m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); - if (sender != null) - sender.m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + if (tp >= NetMessageType.Unused1 && tp <= NetMessageType.Unused29) + { + ThrowOrLog("Unexpected NetMessageType: " + tp); + return; + } - } while (m_socket.Available > 0); - } + try + { + if (tp >= NetMessageType.LibraryError) + { + if (sender != null) + sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); + else + ReceivedUnconnectedLibraryMessage(now, ipsender, tp, ptr, payloadByteLength); + } + else + { + if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) + return; // dropping unconnected message since it's not enabled - /// + NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); + msg.m_isFragment = isFragment; + msg.m_receiveTime = now; + msg.m_sequenceNumber = sequenceNumber; + msg.m_receivedMessageType = tp; + msg.m_senderConnection = sender; + msg.m_senderEndPoint = ipsender; + msg.m_bitLength = payloadBitLength; + + Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); + if (sender != null) + { + if (tp == NetMessageType.Unconnected) + { + // We're connected; but we can still send unconnected messages to this peer + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + else + { + // connected application (non-library) message + sender.ReceivedMessage(msg); + } + } + else + { + // at this point we know the message type is enabled + // unconnected application (non-library) message + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + } + } + catch (Exception ex) + { + LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + } + ptr += payloadByteLength; + } + + m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + if (sender != null) + sender.m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + } + + /// /// If NetPeerConfiguration.AutoFlushSendQueue() is false; you need to call this to send all messages queued using SendMessage() /// public void FlushSendQueue() diff --git a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs index 8a255010e..6ef891f99 100644 --- a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -132,28 +132,36 @@ namespace Lidgren.Network catch { } } + //Avoids allocation on mapping to IPv6 + private IPEndPoint targetCopy = new IPEndPoint(IPAddress.Any, 0); + internal bool ActuallySendPacket(byte[] data, int numBytes, NetEndPoint target, out bool connectionReset) { connectionReset = false; - - target = target.MapToFamily(m_socket.AddressFamily); - - IPAddress ba = default(IPAddress); + IPAddress ba = default(IPAddress); try { ba = NetUtility.GetCachedBroadcastAddress(); - // TODO: refactor this check outta here - if (target.Address == ba) - { - // Some networks do not allow - // a global broadcast so we use the BroadcastAddress from the configuration - // this can be resolved to a local broadcast addresss e.g 192.168.x.255 - target.Address = m_configuration.BroadcastAddress; - m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); - } + // TODO: refactor this check outta here + if (target.Address.Equals(ba)) + { + // Some networks do not allow + // a global broadcast so we use the BroadcastAddress from the configuration + // this can be resolved to a local broadcast addresss e.g 192.168.x.255 + targetCopy.Address = m_configuration.BroadcastAddress; + targetCopy.Port = target.Port; + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + } + else if(m_configuration.DualStack && m_configuration.LocalAddress.AddressFamily == AddressFamily.InterNetworkV6) + NetUtility.CopyEndpoint(target, targetCopy); //Maps to IPv6 for Dual Mode + else + { + targetCopy.Port = target.Port; + targetCopy.Address = target.Address; + } - int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, target); + int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, targetCopy); if (numBytes != bytesSent) LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); @@ -181,7 +189,7 @@ namespace Lidgren.Network } finally { - if (target.Address == ba) + if (target.Address.Equals(ba)) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); } return true; @@ -311,4 +319,4 @@ namespace Lidgren.Network } #endif } -} \ No newline at end of file +} diff --git a/Libraries/Lidgren.Network/NetPeer.cs b/Libraries/Lidgren.Network/NetPeer.cs index 538a87996..78220b565 100644 --- a/Libraries/Lidgren.Network/NetPeer.cs +++ b/Libraries/Lidgren.Network/NetPeer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Collections.Generic; using System.Net; - +using System.Net.Sockets; #if !__NOIPENDPOINT__ using NetEndPoint = System.Net.IPEndPoint; #endif @@ -121,9 +121,16 @@ namespace Lidgren.Network m_connections = new List(); m_connectionLookup = new Dictionary(); m_handshakes = new Dictionary(); - m_senderRemote = (EndPoint)new NetEndPoint(IPAddress.IPv6Any, 0); - m_status = NetPeerStatus.NotRunning; - m_receivedFragmentGroups = new Dictionary>(); + if (m_configuration.LocalAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.IPv6Any, 0); + } + else + { + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + } + m_status = NetPeerStatus.NotRunning; + m_receivedFragmentGroups = new Dictionary>(); } /// @@ -292,10 +299,10 @@ namespace Lidgren.Network { if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint"); + if(m_configuration.DualStack) + remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint); - remoteEndPoint = remoteEndPoint.MapToFamily(m_socket.AddressFamily); - - lock (m_connections) + lock (m_connections) { if (m_status == NetPeerStatus.NotRunning) throw new NetException("Must call Start() first"); diff --git a/Libraries/Lidgren.Network/NetPeerConfiguration.cs b/Libraries/Lidgren.Network/NetPeerConfiguration.cs index b714edae2..3a124900a 100644 --- a/Libraries/Lidgren.Network/NetPeerConfiguration.cs +++ b/Libraries/Lidgren.Network/NetPeerConfiguration.cs @@ -35,12 +35,12 @@ namespace Lidgren.Network // -4 bytes to be on the safe side and align to 8-byte boundary // Total 1408 bytes // Note that lidgren headers (5 bytes) are not included here; since it's part of the "mtu payload" - + /// /// Default MTU value in bytes /// public const int kDefaultMTU = 1408; - + private const string c_isLockedMessage = "You may not modify the NetPeerConfiguration after it has been used to initialize a NetPeer"; private bool m_isLocked; @@ -48,6 +48,8 @@ namespace Lidgren.Network private string m_networkThreadName; private IPAddress m_localAddress; private IPAddress m_broadcastAddress; + private bool m_dualStack; + internal bool m_acceptIncomingConnections; internal int m_maximumConnections; internal int m_defaultOutgoingMessageCapacity; @@ -93,8 +95,8 @@ namespace Lidgren.Network // m_disabledTypes = NetIncomingMessageType.ConnectionApproval | NetIncomingMessageType.UnconnectedData | NetIncomingMessageType.VerboseDebugMessage | NetIncomingMessageType.ConnectionLatencyUpdated | NetIncomingMessageType.NatIntroductionSuccess; m_networkThreadName = "Lidgren network thread"; - m_localAddress = IPAddress.IPv6Any; - m_broadcastAddress = IPAddress.Broadcast; + m_localAddress = IPAddress.Any; + m_broadcastAddress = IPAddress.Broadcast; var ip = NetUtility.GetBroadcastAddress(); if (ip != null) { @@ -142,12 +144,6 @@ namespace Lidgren.Network get { return m_appIdentifier; } } - public bool UseDualModeSockets - { - get; - set; - } = true; - /// /// Enables receiving of the specified type of message /// @@ -332,10 +328,11 @@ namespace Lidgren.Network m_suppressUnreliableUnorderedAcks = value; } } - /// - /// Gets or sets the local ip address to bind to. Defaults to . Cannot be changed once NetPeer is initialized. - /// - public IPAddress LocalAddress + + /// + /// Gets or sets the local ip address to bind to. Defaults to IPAddress.Any. Cannot be changed once NetPeer is initialized. + /// + public IPAddress LocalAddress { get { return m_localAddress; } set @@ -346,10 +343,30 @@ namespace Lidgren.Network } } - /// - /// Gets or sets the local broadcast address to use when broadcasting - /// - public IPAddress BroadcastAddress + /// + /// Gets or sets a value indicating whether the library should use IPv6 dual stack mode. + /// If you enable this you should make sure that the is an IPv6 address. + /// Cannot be changed once NetPeer is initialized. + /// + public bool DualStack + { + get { return m_dualStack; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_dualStack = value; + if (m_dualStack && m_localAddress.Equals(IPAddress.Any)) + m_localAddress = IPAddress.IPv6Any; + if (!m_dualStack && m_localAddress.Equals(IPAddress.IPv6Any)) + m_localAddress = IPAddress.Any; + } + } + + /// + /// Gets or sets the local broadcast address to use when broadcasting + /// + public IPAddress BroadcastAddress { get { return m_broadcastAddress; } set diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index cf5996e98..600447a43 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -98,8 +98,8 @@ namespace Lidgren.Network NetAddress ipAddress = null; if (NetAddress.TryParse(ipOrHost, out ipAddress)) { - if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) - { + if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + { callback(ipAddress); return; } @@ -163,7 +163,7 @@ namespace Lidgren.Network } } - /// + /// /// Get IPv4 address from notation (xxx.xxx.xxx.xxx) or hostname /// public static NetAddress Resolve(string ipOrHost) @@ -176,22 +176,22 @@ namespace Lidgren.Network NetAddress ipAddress = null; if (NetAddress.TryParse(ipOrHost, out ipAddress)) { - if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) - return ipAddress; - throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses"); - } + if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + return ipAddress; + throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses"); + } - // ok must be a host name - try + // ok must be a host name + try { var addresses = Dns.GetHostAddresses(ipOrHost); if (addresses == null) return null; foreach (var address in addresses) { - if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) - return address; - } + if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) + return address; + } return null; } catch (SocketException ex) @@ -240,7 +240,7 @@ namespace Lidgren.Network } return new string(c); } - + /// /// Returns true if the endpoint supplied is on the same subnet as this host /// @@ -282,6 +282,18 @@ namespace Lidgren.Network return bits; } + /// + /// Returns how many bits are necessary to hold a certain number + /// + [CLSCompliant(false)] + public static int BitsToHoldUInt64(ulong value) + { + int bits = 1; + while ((value >>= 1) != 0) + bits++; + return bits; + } + /// /// Returns how many bytes are required to hold a certain number of bits /// @@ -401,7 +413,7 @@ namespace Lidgren.Network { if (j >= h) { - if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.OrdinalIgnoreCase) > 0) + if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.InvariantCulture) > 0) { list[j] = list[j - h]; j -= h; @@ -454,24 +466,28 @@ namespace Lidgren.Network return ComputeSHAHash(bytes, 0, bytes.Length); } - internal static IPAddress MapToFamily(this IPAddress address, AddressFamily family) - { - switch (family) - { - case AddressFamily.InterNetworkV6: - return address.MapToIPv6(); - case AddressFamily.InterNetwork: - return address.MapToIPv4(); - default: - throw new Exception($"Unsupported address family: {family}"); - } - } + /// + /// Copies from to . Maps to an IPv6 address + /// + /// Source. + /// Destination. + internal static void CopyEndpoint(IPEndPoint src, IPEndPoint dst) + { + dst.Port = src.Port; + if (src.AddressFamily == AddressFamily.InterNetwork) + dst.Address = src.Address.MapToIPv6(); + else + dst.Address = src.Address; + } - internal static IPEndPoint MapToFamily(this IPEndPoint endpoint, AddressFamily family) - { - return endpoint.Address.AddressFamily == family - ? endpoint - : new IPEndPoint(endpoint.Address.MapToFamily(family), endpoint.Port); - } + /// + /// Maps the IPEndPoint object to an IPv6 address. Has allocation + /// + internal static IPEndPoint MapToIPv6(IPEndPoint endPoint) + { + if (endPoint.AddressFamily == AddressFamily.InterNetwork) + return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port); + return endPoint; + } } -} \ No newline at end of file +} From 12e43d95efcb6cf4823863002eac8cceed7a7d50 Mon Sep 17 00:00:00 2001 From: Markus Isberg Date: Thu, 5 Oct 2023 14:55:53 +0300 Subject: [PATCH 11/14] Unstable 1.1.15.0 --- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../CircuitBox/CircuitBoxConnection.cs | 2 +- .../Events/EventActions/CheckItemAction.cs | 14 +++++----- .../SharedSource/Events/MonsterEvent.cs | 2 ++ .../Items/Components/ElectricalDischarger.cs | 25 ++++++++--------- .../SharedSource/Items/Components/Turret.cs | 27 ++++++++----------- .../SharedSource/Map/Levels/Level.cs | 3 +++ Barotrauma/BarotraumaShared/changelog.txt | 10 +++++++ 13 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index aa20c1261..348eb1f8f 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.14.0 + 1.1.15.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 75a4a7c97..01d8301a5 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.14.0 + 1.1.15.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 77b4d8ebb..b41d5ab77 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.14.0 + 1.1.15.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5246e818e..7be5d124f 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.14.0 + 1.1.15.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 1959e3b21..fe58389a0 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.14.0 + 1.1.15.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 898f1765a..ad41b7b61 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.14.0 + 1.1.15.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs index 54eb1ef7e..cd2b6bdb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs @@ -25,7 +25,7 @@ namespace Barotrauma if (connector is CircuitBoxOutputConnection output) { output.ReceiveSignal(signal); - return; + continue; } Connection.SendSignalIntoConnection(signal, connector.Connection); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 4d53e6eca..cd0200646 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -133,15 +133,13 @@ namespace Barotrauma tempTargetItems.Clear(); foreach (var target in targets) { - if (target is not Item item) { continue; } - if (itemTags.Any() && itemTags.None(item.HasTag) && - itemIdentifierSplit.Any() && !itemIdentifierSplit.Contains(item.Prefab.Identifier)) + if (target is not Item item) { continue; } + if (itemTags.Any(item.HasTag) || itemIdentifierSplit.Contains(item.Prefab.Identifier)) { - continue; - } - if (ConditionalsMatch(item, character: null)) - { - tempTargetItems.Add(item); + if (ConditionalsMatch(item, character: null)) + { + tempTargetItems.Add(item); + } } } if (EnoughTargets(targetCount, tempTargetItems.Count)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 00572c070..71eebca71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -611,6 +611,8 @@ namespace Barotrauma { //round ended before the coroutine finished if (GameMain.GameSession == null || Level.Loaded == null) { return; } + + if (monster.Removed) { return; } System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 39fd2ed7f..a1b8e5738 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -181,24 +182,20 @@ namespace Barotrauma.Items.Components if (hasPower) { var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); - int batteryCount = batteries.Count(); - if (batteryCount > 0) + float neededPower = PowerConsumption; + while (neededPower > 0.0001f && batteries.Any()) { - float neededPower = PowerConsumption; - while (neededPower > 0.0001f) + float takePower = neededPower / batteries.Count(); + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) { - float takePower = neededPower / batteryCount; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) - { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; #if SERVER - if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } + if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } #endif - } } - } + } Discharge(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 9c29503ad..71e414e97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -773,28 +773,23 @@ namespace Barotrauma.Items.Components if (!ignorePower) { var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); - int batteryCount = batteries.Count(); - if (batteryCount > 0) + float neededPower = GetPowerRequiredToShoot(); + // tinkering is currently not factored into the common method as it is checked only when shooting + // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later + neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); + while (neededPower > 0.0001f && batteries.Any()) { - float neededPower = GetPowerRequiredToShoot(); - // tinkering is currently not factored into the common method as it is checked only when shooting - // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later - neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); - while (neededPower > 0.0001f) + float takePower = neededPower / batteries.Count(); + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) { - float takePower = neededPower / batteryCount; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) - { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; #if SERVER - battery.Item.CreateServerEvent(battery); + battery.Item.CreateServerEvent(battery); #endif - } } } - } launchedProjectile = projectiles.FirstOrDefault(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index a3fa754af..d3efea804 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1378,6 +1378,9 @@ namespace Barotrauma { if (sub.Info.IsOutpost) { +#if CLIENT + if (GameMain.GameSession.GameMode is TutorialMode) { continue; } +#endif OutpostGenerator.PowerUpOutpost(sub); } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 44eaec1b1..4bcd23924 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,13 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.1.15.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed console errors when there are swarm feeders (or other monsters that immediately despawn when killed) in an irradiated level. +- Fixed occasional crashes when firing a turret makes a supercapacitor drop to 0% charge. +- Fixed circuit box only outputting to one pin when wired directly from input to multiple output pins. +- Fixed tutorial reactor starting already powered up, making the first engineer tutorial objectives complete immediately. +- Miscellaneous tutorial fixes. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.1.14.0 ------------------------------------------------------------------------------------------------------------------------------------------------- From ca2c30edca3e0edf6ad69cf35ea62024793c4730 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 13 Oct 2023 19:06:29 +0300 Subject: [PATCH 12/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c327e282c..37575d177 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.0.21.0 - - Unstable (v1.1.14.0) + - Unstable (v1.1.16.0) - Other validations: required: true From 34ffc520cc95c7d693823f19feef45f6063b1894 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 19 Oct 2023 17:18:51 +0300 Subject: [PATCH 13/14] v1.1.18.0 (Treacherous Tides Update) --- .../Items/Components/Projectile.cs | 2 + .../ClientSource/Items/Components/Turret.cs | 7 --- .../ClientSource/Map/MapEntity.cs | 18 +++++-- .../ClientSource/Map/WayPoint.cs | 19 +++++-- .../Screens/EventEditor/EditorNode.cs | 5 ++ .../ClientSource/Screens/SubEditorScreen.cs | 32 +++++++++--- .../ClientSource/SubEditorCommands.cs | 50 +++++++++++++++---- .../Items/Components/ItemLabel.cs | 7 +++ .../Items/Components/Projectile.cs | 1 + .../AI/Objectives/AIObjectiveManager.cs | 3 +- .../SharedSource/DebugConsole.cs | 29 ++++++++++- .../Events/Missions/MissionPrefab.cs | 12 ++++- .../SharedSource/GameSession/Data/Factions.cs | 3 +- .../GameSession/Data/Reputation.cs | 5 +- .../SharedSource/GameSession/GameSession.cs | 5 ++ .../Items/Components/Projectile.cs | 3 ++ .../SharedSource/Items/Components/Turret.cs | 7 +++ .../SharedSource/Items/ItemPrefab.cs | 6 ++- .../SharedSource/Map/Explosion.cs | 4 ++ .../Map/Outposts/OutpostGenerator.cs | 4 +- 20 files changed, 179 insertions(+), 43 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 7993a8a97..fd794ea11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -21,9 +21,11 @@ namespace Barotrauma.Items.Components Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); spreadIndex = msg.ReadByte(); + ushort submarineID = msg.ReadUInt16(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); + item.Submarine = Entity.FindEntityByID(submarineID) as Submarine; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 5e5cc982f..a4903a351 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -57,13 +57,6 @@ namespace Barotrauma.Items.Components private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); - [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] - public Color HudTint - { - get; - private set; - } - [Serialize(false, IsPropertySaveable.No, description: "Should the charge of the connected batteries/supercapacitors be shown at the top of the screen when operating the item.")] public bool ShowChargeIndicator { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index e487bb33d..7fefcda46 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -163,10 +163,22 @@ namespace Barotrauma { if (SelectedAny) { - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); + 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(); + } } - SelectedList.ForEach(e => { if (!e.Removed) { e.Remove(); } }); - SelectedList.Clear(); } if (PlayerInput.IsCtrlDown()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index bf77b5ccc..d48f4c959 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -60,6 +60,7 @@ namespace Barotrauma } Sprite sprite = iconSprites[SpawnType.ToString()]; + Sprite sprite2 = null; if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) { sprite = iconSprites["Path"]; @@ -67,17 +68,29 @@ namespace Barotrauma else if (ConnectedDoor != null) { sprite = iconSprites["Door"]; - if (ConnectedDoor.IsHorizontal && Ladders == null) + if (Ladders != null) { + sprite2 = iconSprites["Ladder"]; + } + else if (ConnectedDoor.IsHorizontal) + { + //connected to a hatch but not ladders, something's probably off here clr = Color.Yellow; } + if (!Submarine.RectContains(ConnectedDoor.Item.WorldRect, WorldPosition)) + { + clr = Color.Red; + } } else if (Ladders != null) { sprite = iconSprites["Ladder"]; } - sprite.Draw(spriteBatch, drawPos, clr, scale: iconSize / (float)sprite.SourceRect.Width, depth: 0.001f); - sprite.RelativeOrigin = Vector2.One * 0.5f; + + float spriteScale = iconSize / (float)sprite.SourceRect.Width; + sprite.Draw(spriteBatch, drawPos, clr, origin: sprite.size / 2, scale: spriteScale, depth: 0.001f); + sprite2?.Draw(spriteBatch, drawPos + sprite.size * spriteScale * 0.5f, clr, origin: sprite2.size / 2, scale: spriteScale, depth: 0.001f); + if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) { AssignedJob.Icon.Draw(spriteBatch, drawPos, AssignedJob.UIColor, scale: iconSize / (float)AssignedJob.Icon.SourceRect.Width * 0.8f, depth: 0.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index c5424e4ba..d0406a244 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -273,6 +273,11 @@ namespace Barotrauma GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); } + public void AddConnection(NodeConnectionType connectionType) + { + Connections.Add(new EventEditorNodeConnection(this, connectionType)); + } + public virtual void AddOption() { Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Option)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index bbae3c7c7..1fd3ca751 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -16,6 +16,9 @@ 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; @@ -3915,18 +3918,31 @@ 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: delegate - { - StoreCommand(new AddOrDeleteCommand(targets, true)); - foreach (var me in targets) - { - if (!me.Removed) { me.Remove(); } - } - }), + 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(); } + } + } + } + private void MoveToLayer(string layer, List content) { layer ??= string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index 3b9f2241e..39f998cef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Barotrauma.Items.Components; @@ -131,7 +132,7 @@ namespace Barotrauma { foreach (MapEntity receiver in receivers) { - if (receiver is Item it && it.ParentInventory != null) + if (receiver is Item { ParentInventory: not null } it) { PreviousInventories.Add(new InventorySlotItem(it.ParentInventory.FindIndex(it), it), it.ParentInventory); } @@ -192,14 +193,35 @@ namespace Barotrauma public override void Execute() { - DeleteUndelete(true); - ContainedItemsCommand?.ForEach(cmd => cmd.Execute()); + var items = DeleteUndelete(true); + ContainedItemsCommand?.ForEach(static cmd => cmd.Execute()); + CircuitBoxWorkaround(items); } public override void UnExecute() { - DeleteUndelete(false); - ContainedItemsCommand?.ForEach(cmd => cmd.UnExecute()); + var items = DeleteUndelete(false); + ContainedItemsCommand?.ForEach(static cmd => cmd.UnExecute()); + CircuitBoxWorkaround(items); + } + + // FIXME Temporary workaround for circuit boxes throwing console errors and breaking completely when undoing a deletion + private static void CircuitBoxWorkaround(Option> entitiesOption) + { + if (!entitiesOption.TryUnwrap(out var entities)) { return; } + + foreach (var entity in entities) + { + if (entity is not Item it) { continue; } + + if (it.GetComponent() is not null) + { + foreach (var container in it.GetComponents()) + { + container.Inventory.DeleteAllItems(); + } + } + } } public override void Cleanup() @@ -215,10 +237,10 @@ namespace Barotrauma CloneList?.Clear(); Receivers.Clear(); PreviousInventories?.Clear(); - ContainedItemsCommand?.ForEach(cmd => cmd.Cleanup()); + ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup()); } - private void DeleteUndelete(bool redo) + private Option> DeleteUndelete(bool redo) { bool wasDeleted = WasDeleted; @@ -227,13 +249,14 @@ namespace Barotrauma if (wasDeleted) { - Debug.Assert(Receivers.All(entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); + Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); List clones = MapEntity.Clone(CloneList); int length = Math.Min(Receivers.Count, clones.Count); for (int i = 0; i < length; i++) { - MapEntity clone = clones[i], receiver = Receivers[i]; + MapEntity clone = clones[i], + receiver = Receivers[i]; if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem) { @@ -245,7 +268,7 @@ namespace Barotrauma { case null: continue; - case ItemContainer newContainer when newContainer.Inventory != null && ic is ItemContainer itemContainer && itemContainer.Inventory != null: + case ItemContainer { Inventory: not null } newContainer when ic is ItemContainer { Inventory: not null } itemContainer: itemContainer.Inventory.GetReplacementOrThiS().ReplacedBy = newContainer.Inventory; goto default; default: @@ -260,7 +283,8 @@ namespace Barotrauma for (int i = 0; i < length; i++) { - MapEntity clone = clones[i], receiver = Receivers[i]; + MapEntity clone = clones[i], + receiver = Receivers[i]; if (clone is Item it) { @@ -278,6 +302,8 @@ namespace Barotrauma { clone.Submarine = Submarine.MainSub; } + + return Option.Some(clones.ToImmutableArray()); } else { @@ -289,6 +315,8 @@ namespace Barotrauma receiver.Remove(); } } + + return Option.None; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index dbcf85e4d..81610eb59 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -18,6 +18,13 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreLocalization + { + get; + set; + } + [Editable, Serialize("0,0,0,255", IsPropertySaveable.Yes, description: "The color of the text displayed on the label.", alwaysUseInstanceValues: true)] public Color TextColor { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index c1f24ccf6..00f22651c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -35,6 +35,7 @@ namespace Barotrauma.Items.Components msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); msg.WriteByte(eventData.SpreadCounter); + msg.WriteUInt16(LaunchSub?.ID ?? Entity.NullEntityID); } bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 40be336b8..5d1aa4a61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -154,7 +154,8 @@ namespace Barotrauma } var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && + Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index af66e1865..552fe6a3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1188,6 +1188,33 @@ namespace Barotrauma throw new Exception("crash command issued"); })); + commands.Add(new Command("listeditableproperties", "", (string[] args) => + { + StringBuilder sb = new StringBuilder(); + string filename; +#if CLIENT + filename = "ItemComponent properties (client).txt"; + sb.AppendLine("Client-side ItemComponent properties:"); +#else + filename = "ItemComponent properties (server).txt"; + sb.AppendLine("Server-side ItemComponent properties:"); +#endif + var itemComponents = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))); + foreach (var ic in itemComponents.OrderBy(ic => ic.Name)) + { + sb.AppendLine(ic.Name+":"); + foreach (var prop in ic.GetProperties()) + { + if (prop.DeclaringType != ic) { continue; } + if (prop.GetCustomAttributes(inherit: false).OfType().Any()) + { + sb.AppendLine(prop.Name); + } + } + } + File.WriteAllText(filename, sb.ToString()); + })); + commands.Add(new Command("fastforward", "fastforward [seconds]: Fast forwards the game by x seconds. Note that large numbers may cause a long freeze.", (string[] args) => { float seconds = 0; @@ -1300,7 +1327,7 @@ namespace Barotrauma } #endif - commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) => + commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) => { if (GameMain.GameSession?.GameMode is CampaignMode campaign) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 5add25d3c..ef4c76774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -132,6 +132,11 @@ namespace Barotrauma /// public readonly List AllowedLocationTypes = new List(); + /// + /// The mission can only happen in locations owned by this faction. In the mission mode, the location is forced to be owned by this faction. + /// + public readonly Identifier RequiredLocationFaction; + /// /// Show entities belonging to these sub categories when the mission starts /// @@ -198,6 +203,7 @@ namespace Barotrauma RequireWreck = element.GetAttributeBool("requirewreck", false); RequireRuin = element.GetAttributeBool("requireruin", false); BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); + RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty); Commonness = element.GetAttributeInt("commonness", 1); AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true); if (element.GetAttribute("difficulty") != null) @@ -378,7 +384,11 @@ namespace Barotrauma { if (from == to) { - return + if (!RequiredLocationFaction.IsEmpty && from.Faction?.Prefab.Identifier != RequiredLocationFaction) + { + return false; + } + return AllowedLocationTypes.Any(lt => lt == "any") || AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) || AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 548d30d55..d39be4e1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; namespace Barotrauma { @@ -18,7 +17,7 @@ namespace Barotrauma public Reputation Reputation { get; } public FactionPrefab Prefab { get; } - public Faction(CampaignMetadata metadata, FactionPrefab prefab) + public Faction(CampaignMetadata? metadata, FactionPrefab prefab) { Prefab = prefab; Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 50e9ff216..1ff492a18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -42,10 +42,10 @@ namespace Barotrauma public float Value { - get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation)); + get => Metadata == null ? 0 : Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation)); private set { - if (MathUtils.NearlyEqual(Value, value)) { return; } + if (MathUtils.NearlyEqual(Value, value) || Metadata == null) { return; } float prevValue = Value; @@ -137,7 +137,6 @@ namespace Barotrauma private Reputation(CampaignMetadata metadata, Faction faction, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation) { - System.Diagnostics.Debug.Assert(metadata != null); System.Diagnostics.Debug.Assert(faction != null || location != null); Metadata = metadata; Identifier = identifier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 115d3dce2..5a52f982e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -388,6 +388,11 @@ namespace Barotrauma .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)) .GetRandom(rand); dummyLocations = CreateDummyLocations(levelSeed, locationType); + if (!mission.Prefab.RequiredLocationFaction.IsEmpty && + FactionPrefab.Prefabs.TryGet(mission.Prefab.RequiredLocationFaction, out var factionPrefab)) + { + dummyLocations[0].Faction = dummyLocations[1].Faction = new Faction(metadata: null, factionPrefab); + } randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index b9146e660..2af67598e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -83,6 +83,7 @@ namespace Barotrauma.Items.Components public Attack Attack { get; private set; } private Vector2 launchPos; + public Submarine LaunchSub; private readonly HashSet hits = new HashSet(); @@ -362,6 +363,7 @@ namespace Barotrauma.Items.Components User = user; if (Item.Removed) { return; } launchPos = simPosition; + LaunchSub = item.Submarine; //set the rotation of the projectile again because dropping the projectile resets the rotation Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false); if (DeactivationTime > 0) @@ -478,6 +480,7 @@ namespace Barotrauma.Items.Components Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; + LaunchSub = item.Submarine; item.body.Enabled = true; if (item.body.BodyType == BodyType.Kinematic) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 71e414e97..0ad0227b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -369,6 +369,13 @@ namespace Barotrauma.Items.Components [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable] public Identifier FriendlyTag { get; private set; } + [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] + public Color HudTint + { + get; + private set; + } + public Turret(Item item, ContentXElement element) : base(item, element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 27e58824a..dd3abc0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1129,7 +1129,11 @@ namespace Barotrauma var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); + //it's ok for variants to clear the primary and secondary containers to disable the PreferredContainer element + if (variantOf == null) + { + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index dce56e175..db32a7ec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -173,6 +173,8 @@ namespace Barotrauma public readonly HashSet IgnoredSubmarines = new HashSet(); + public readonly HashSet IgnoredCharacters = new HashSet(); + /// /// Strength of the EMP effect created by the explosion. /// @@ -437,6 +439,8 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { + if (IgnoredCharacters.Contains(c)) { continue; } + if (!c.Enabled || Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange || Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 5c0da41f4..ba0a62262 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -490,8 +490,8 @@ namespace Barotrauma foreach (var moduleCount in generationParams.ModuleCounts) { if (!moduleCount.RequiredFaction.IsEmpty && - location.Faction?.Prefab.Identifier != moduleCount.RequiredFaction && - location.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction) + location?.Faction?.Prefab.Identifier != moduleCount.RequiredFaction && + location?.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction) { continue; } From 331565f726920b6fee11fccf9d26415670f436b8 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 19 Oct 2023 17:23:12 +0300 Subject: [PATCH 14/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 37575d177..ceda01f73 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -73,8 +73,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.0.21.0 - - Unstable (v1.1.16.0) + - v1.1.18.0 (Treacherous Tides) - Other validations: required: true
  • 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⩔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