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] 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); } ///