From caa5a2f76293b516a1e9e504c498168eeb021c3e Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Wed, 11 Jan 2023 15:36:23 +0200 Subject: [PATCH] Faction Test 100.13.0.0 --- .../Characters/Animation/Ragdoll.cs | 18 +- .../ClientSource/Characters/CharacterInfo.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 5 +- .../Events/EventActions/ConversationAction.cs | 55 ++-- .../ClientSource/Events/EventManager.cs | 79 +++--- .../ClientSource/Events/Missions/Mission.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 4 +- .../ClientSource/GUI/TalentMenu.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 256 +++++++++++++----- .../BarotraumaClient/ClientSource/GameMain.cs | 33 +-- .../GameModes/SinglePlayerCampaign.cs | 5 +- .../ClientSource/GameSession/RoundSummary.cs | 127 ++++----- .../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/Machines/Sonar.cs | 9 +- .../ClientSource/Items/Inventory.cs | 84 +----- .../ClientSource/Items/ItemPrefab.cs | 6 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 4 +- .../ClientSource/Map/Map/Map.cs | 66 ++--- .../ClientSource/Map/MapEntity.cs | 4 +- .../ClientSource/Map/RoundSound.cs | 5 +- .../ClientSource/Networking/BanList.cs | 19 +- .../Networking/ChildServerRelay.cs | 1 + .../ClientSource/Networking/GameClient.cs | 76 +++--- .../ClientEntityEventManager.cs | 19 +- .../Networking/Primitives/Peers/ClientPeer.cs | 2 + .../CampaignSetupUI/CampaignSetupUI.cs | 2 +- .../SinglePlayerCampaignSetupUI.cs | 2 +- .../ClientSource/Screens/CampaignUI.cs | 2 +- .../CharacterEditor/CharacterEditorScreen.cs | 11 +- .../Screens/EventEditor/EventEditorScreen.cs | 9 + .../ClientSource/Screens/MainMenuScreen.cs | 88 ++++-- .../ClientSource/Screens/SlideshowPlayer.cs | 2 +- .../ClientSource/Screens/SubEditorScreen.cs | 17 +- .../ClientSource/Settings/SettingsMenu.cs | 2 + .../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 | 72 +++-- .../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 | 11 +- .../Events/EventActions/ConversationAction.cs | 16 +- .../ServerSource/Events/EventManager.cs | 12 +- .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../Items/Components/Projectile.cs | 2 +- .../ServerSource/Items/Item.cs | 10 +- .../ServerSource/Map/Submarine.cs | 7 +- .../ServerSource/Networking/GameServer.cs | 7 +- .../BarotraumaServer/WindowsServer.csproj | 3 +- .../Data/campaignsettings.xml | 5 +- .../Characters/AI/AIController.cs | 15 + .../Characters/AI/EnemyAIController.cs | 13 +- .../Characters/AI/HumanAIController.cs | 67 +++-- .../Characters/AI/NPCConversation.cs | 1 + .../AI/Objectives/AIObjectiveContainItem.cs | 8 +- .../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 | 14 +- .../Characters/Animation/Ragdoll.cs | 43 ++- .../SharedSource/Characters/Attack.cs | 90 ++++-- .../SharedSource/Characters/Character.cs | 31 ++- .../SharedSource/Characters/CharacterInfo.cs | 7 +- .../Characters/Health/CharacterHealth.cs | 44 ++- .../SharedSource/Characters/Limb.cs | 86 ++++-- .../AbilityConditionAttackData.cs | 7 +- .../CharacterAbilityGiveItemStatToTags.cs | 8 + .../Abilities/CharacterAbilityModifyStat.cs | 5 + ...erAbilityUnlockApprenticeshipTalentTree.cs | 4 + .../AbilityGroups/CharacterAbilityGroup.cs | 10 + .../ContentPackageManager.cs | 2 + .../ContentManagement/ContentXElement.cs | 1 + .../SharedSource/DebugConsole.cs | 24 +- .../BarotraumaShared/SharedSource/Enums.cs | 2 +- .../Events/EventActions/ConversationAction.cs | 2 +- .../Events/EventActions/MissionAction.cs | 7 +- .../SharedSource/Events/EventManager.cs | 1 + .../Missions/AbandonedOutpostMission.cs | 4 +- .../Events/Missions/BeaconMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 22 +- .../SharedSource/Events/MonsterEvent.cs | 2 +- .../GameAnalytics/GameAnalyticsManager.cs | 2 +- .../SharedSource/GameSession/CrewManager.cs | 11 + .../GameSession/Data/CampaignMetadata.cs | 3 +- .../SharedSource/GameSession/Data/Factions.cs | 11 +- .../GameSession/GameModes/CampaignMode.cs | 29 +- .../GameSession/GameModes/CampaignSettings.cs | 2 +- .../GameModes/MultiPlayerCampaign.cs | 10 +- .../GameSession/UpgradeManager.cs | 29 +- .../Items/Components/DockingPort.cs | 20 +- .../SharedSource/Items/Components/Door.cs | 10 +- .../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/ItemContainer.cs | 1 - .../Items/Components/Machines/Controller.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Projectile.cs | 30 +- .../SharedSource/Items/Components/Quality.cs | 10 +- .../SharedSource/Items/Components/Rope.cs | 7 +- .../Items/Components/Signal/LightComponent.cs | 31 ++- .../SharedSource/Items/Components/Turret.cs | 4 +- .../SharedSource/Items/Inventory.cs | 7 +- .../SharedSource/Items/Item.cs | 41 ++- .../SharedSource/Items/ItemEventData.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 24 +- .../SharedSource/Map/Levels/LevelData.cs | 6 +- .../Map/Levels/LevelGenerationParams.cs | 29 +- .../SharedSource/Map/Map/Location.cs | 17 +- .../SharedSource/Map/Map/Map.cs | 14 +- .../Map/Outposts/OutpostGenerator.cs | 32 ++- .../SharedSource/Map/Submarine.cs | 91 ++++--- .../SharedSource/Map/SubmarineBody.cs | 6 +- .../Networking/INetSerializable.cs | 2 +- .../SharedSource/Networking/NetworkMember.cs | 13 + .../Primitives/NetworkPeerStructs.cs | 2 + .../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 | 116 +++++++- .../SharedSource/Utils/Range.cs | 2 +- .../SharedSource/Utils/Result.cs | 4 +- Barotrauma/BarotraumaShared/changelog.txt | 173 +++++++++--- ...tSerializableStructImplementationChecks.cs | 103 ++++--- .../INetSerializableStructTests.cs | 32 ++- .../Facepunch.Steamworks/Structs/UgcItem.cs | 16 ++ 145 files changed, 2100 insertions(+), 1111 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index cd3614aea..6344495b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -444,10 +444,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/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index c2d6b2e4c..73c92f5d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -946,7 +946,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/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 18e93bfbd..0d7bb2e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -83,6 +83,7 @@ namespace Barotrauma GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), selectedButton: null); // gray out the last text block if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) { @@ -269,14 +270,7 @@ namespace Barotrauma if (actionInstance != null) { actionInstance.selectedOption = selectedOption; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); btn.ExternalHighlight = true; return true; } @@ -286,14 +280,7 @@ namespace Barotrauma SendResponse(actionId.Value, selectedOption); btn.CanBeFocused = false; btn.ExternalHighlight = true; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); return true; } //should not happen @@ -305,6 +292,18 @@ namespace Barotrauma } } + public static void SelectOption(ushort actionId, int option) + { + if (lastMessageBox.UserData is Pair userData) + { + if (userData.Second != actionId) { return; } + + GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; + Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), (btn) => btn.UserData is int i && i == option); + } + } + private static Tuple GetSizes(DialogTypes dialogTypes) { return dialogTypes switch @@ -383,6 +382,30 @@ namespace Barotrauma return buttons; } + private static void DisableButtons(IEnumerable buttons, GUIButton selectedButton) + { + DisableButtons(buttons, (btn) => btn == selectedButton); + } + + private static void DisableButtons(IEnumerable buttons, Func isSelectedButton) + { + foreach (GUIButton btn in buttons) + { + if (btn.CanBeFocused) + { + btn.CanBeFocused = false; + if (isSelectedButton(btn)) + { + btn.Selected = true; + } + else + { + btn.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + } + } + private static void SendResponse(UInt16 actionId, int selectedOption) { IWriteMessage outmsg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 2405fd5db..9207f0ddd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -608,47 +608,56 @@ namespace Barotrauma } break; case NetworkEventType.CONVERSATION: - UInt16 identifier = msg.ReadUInt16(); - string eventSprite = msg.ReadString(); - byte dialogType = msg.ReadByte(); - bool continueConversation = msg.ReadBoolean(); - UInt16 speakerId = msg.ReadUInt16(); - string text = msg.ReadString(); - bool fadeToBlack = msg.ReadBoolean(); - byte optionCount = msg.ReadByte(); - List options = new List(); - for (int i = 0; i < optionCount; i++) { - options.Add(msg.ReadString()); - } - - byte endCount = msg.ReadByte(); - int[] endings = new int[endCount]; - for (int i = 0; i < endCount; i++) - { - endings[i] = msg.ReadByte(); - } - - if (string.IsNullOrEmpty(text) && optionCount == 0) - { - GUIMessageBox.MessageBoxes.ForEachMod(mb => + UInt16 identifier = msg.ReadUInt16(); + string eventSprite = msg.ReadString(); + byte dialogType = msg.ReadByte(); + bool continueConversation = msg.ReadBoolean(); + UInt16 speakerId = msg.ReadUInt16(); + string text = msg.ReadString(); + bool fadeToBlack = msg.ReadBoolean(); + byte optionCount = msg.ReadByte(); + List options = new List(); + for (int i = 0; i < optionCount; i++) { - if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + options.Add(msg.ReadString()); + } + + byte endCount = msg.ReadByte(); + int[] endings = new int[endCount]; + for (int i = 0; i < endCount; i++) + { + endings[i] = msg.ReadByte(); + } + + if (string.IsNullOrEmpty(text) && optionCount == 0) + { + GUIMessageBox.MessageBoxes.ForEachMod(mb => { - (mb as GUIMessageBox)?.Close(); - } - }); + if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + { + (mb as GUIMessageBox)?.Close(); + } + }); + } + else + { + ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + } + if (Entity.FindEntityByID(speakerId) is Character speaker) + { + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.SetCustomInteract(null, null); + } + break; } - else + case NetworkEventType.CONVERSATION_SELECTED_OPTION: { - ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + UInt16 identifier = msg.ReadUInt16(); + int selectedOption = msg.ReadByte() - 1; + ConversationAction.SelectOption(identifier, selectedOption); + break; } - if (Entity.FindEntityByID(speakerId) is Character speaker) - { - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.SetCustomInteract(null, null); - } - break; case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); int locationIndex = msg.ReadInt32(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 57a6d591d..410c7ec87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -38,7 +38,7 @@ namespace Barotrauma return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); } - public RichString GetReputationRewardText(Location currLocation) + public RichString GetReputationRewardText() { List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) @@ -46,7 +46,7 @@ namespace Barotrauma FactionPrefab targetFaction; if (reputationReward.Key == "location" ) { - targetFaction = currLocation.Faction?.Prefab; + targetFaction = OriginLocation.Faction?.Prefab; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index cd0680556..7abd6a444 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1563,7 +1563,7 @@ namespace Barotrauma descriptionText += "\n\n" + missionMessage; } RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); - RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + RichString reputationText = mission.GetReputationRewardText(); Func wrapMissionText(GUIFont font) { @@ -1773,7 +1773,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 893a5e843..6f440255e 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 c23fa8c18..94055e5b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Globalization; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -93,6 +92,16 @@ namespace Barotrauma Repairs } + private enum UpgradeStoreUserData + { + BuyButton, + BuyButtonLayout, + ProgressBarLayout, + IncreaseLabel, + PriceLabel, + MaterialCostList + } + public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) { WaitForServerUpdate = false; @@ -605,7 +614,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(); @@ -955,7 +964,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) { @@ -992,11 +1001,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) @@ -1086,13 +1095,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, characterList); 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; @@ -1110,21 +1129,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) @@ -1132,12 +1156,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; @@ -1146,15 +1191,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(); @@ -1180,7 +1223,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) @@ -1202,31 +1245,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), @@ -1245,7 +1278,7 @@ namespace Barotrauma return true; }; - UpdateUpgradeEntry(prefabFrame, prefab, category, Campaign); + UpdateUpgradeEntry(prefabFrame.Frame, prefab, category, Campaign); } private void CreateItemTooltip(MapEntity entity) @@ -1628,7 +1661,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) @@ -1641,36 +1674,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, characterList); + + 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, characterList); - - 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..c15b9bc19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -735,8 +735,8 @@ namespace Barotrauma { Client.Quit(); Client = null; - MainMenuScreen.Select(); } + MainMenuScreen.Select(); if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId)) { @@ -1099,37 +1099,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/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 6a09e313d..6173482f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -85,7 +85,6 @@ namespace Barotrauma /// private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { - CampaignMetadata = new CampaignMetadata(); UpgradeManager = new UpgradeManager(this); Settings = settings; InitFactions(); @@ -107,18 +106,16 @@ namespace Barotrauma private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; - foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "metadata": - CampaignMetadata = new CampaignMetadata(subElement); + CampaignMetadata.Load(subElement); break; } } - CampaignMetadata ??= new CampaignMetadata(); InitFactions(); foreach (var subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 361685dde..949500e0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -21,7 +21,7 @@ namespace Barotrauma private readonly GameMode gameMode; - private readonly Dictionary initialFactionReputations = new Dictionary(); + private readonly Dictionary initialFactionReputations = new Dictionary(); public GUILayoutGroup ButtonArea { get; private set; } @@ -39,7 +39,7 @@ namespace Barotrauma { foreach (Faction faction in campaignMode.Factions) { - initialFactionReputations.Add(faction, faction.Reputation.Value); + initialFactionReputations.Add(faction.Prefab.Identifier, faction.Reputation.Value); } } } @@ -312,17 +312,26 @@ namespace Barotrauma var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(missionMessage), wrap: true); int reward = displayedMission.GetReward(Submarine.MainSub); - if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) + RichString reputationText = displayedMission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); - if (share > 0) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText); + } + + if (reward > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); - RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); + if (share > 0) + { + string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); + RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + } } } } @@ -400,26 +409,10 @@ namespace Barotrauma }; reputationList.ContentBackground.Color = Color.Transparent; - /*if (startLocation.Type.HasOutpost && startLocation.Reputation != null) - { - var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); - var locationFrame = CreateReputationElement( - reputationList.Content, - startLocation.Name, - startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, - startLocation.Type.Name, "", - iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); - CreatePathUnlockElement(locationFrame, null, startLocation); - }*/ - foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; - if (initialFactionReputations.ContainsKey(faction)) - { - initialReputation = initialFactionReputations[faction]; - } - else + if (!initialFactionReputations.TryGetValue(faction.Prefab.Identifier, out initialReputation)) { DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); } @@ -454,50 +447,60 @@ namespace Barotrauma void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) { - if (GameMain.GameSession?.Campaign?.Map != null) + if (GameMain.GameSession?.Campaign?.Map == null) { return; } + + IEnumerable connectionsBetweenBiomes = + GameMain.GameSession.Campaign.Map.Connections.Where(c => c.Locations[0].Biome != c.Locations[1].Biome); + + foreach (LocationConnection connection in connectionsBetweenBiomes) { - foreach (LocationConnection connection in GameMain.GameSession.Campaign.Map.Connections) + if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + + //don't show the "reputation required to unlock" text if another connection between the biomes has already been unlocked + if (connectionsBetweenBiomes.Where(c => !c.Locked).Any(c => + (c.Locations[0].Biome == connection.Locations[0].Biome && c.Locations[1].Biome == connection.Locations[1].Biome) || + (c.Locations[1].Biome == connection.Locations[0].Biome && c.Locations[0].Biome == connection.Locations[1].Biome))) { - if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + continue; + } - var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); + var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); - if (unlockEvent == null) { continue; } - if (unlockEvent.Faction.IsEmpty) + if (unlockEvent == null) { continue; } + if (unlockEvent.Faction.IsEmpty) + { + if (location == null || gateLocation != location) { continue; } + } + else + { + if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } + } + + if (unlockEvent != null) + { + Reputation unlockReputation = gateLocation.Reputation; + Faction unlockFaction = null; + if (!unlockEvent.Faction.IsEmpty) { - if (location == null || gateLocation != location) { continue; } + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); + unlockReputation = unlockFaction?.Reputation; } - else + float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); + RichString unlockText = RichString.Rich(TextManager.GetWithVariables( + "lockedpathreputationrequirement", + ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); + var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); + unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); + unlockInfoPanel.UserData = "unlockinfo"; + if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { - if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } - } - - if (unlockEvent != null) - { - Reputation unlockReputation = gateLocation.Reputation; - Faction unlockFaction = null; - if (!unlockEvent.Faction.IsEmpty) - { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); - unlockReputation = unlockFaction?.Reputation; - } - float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); - RichString unlockText = RichString.Rich(TextManager.GetWithVariables( - "lockedpathreputationrequirement", - ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); - var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, - unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); - unlockInfoPanel.UserData = "unlockinfo"; - if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) - { - unlockInfoPanel.Font = GUIStyle.SmallFont; - } + unlockInfoPanel.Font = GUIStyle.SmallFont; } } - } + } } } 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/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index edcee0dc9..628294d1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1181,13 +1181,18 @@ namespace Barotrauma.Items.Components if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's - if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } + if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && + !dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon) + { + continue; + } //don't show the docking ports of the opposing team on the sonar if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && - dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) + !dockingPort.Item.Submarine.Info.IsOutpost && + !dockingPort.Item.Submarine.Info.IsBeacon) { // specifically checking for friendlyNPC seems more logical here if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } 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/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 39ee585b3..12795d4e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -544,9 +544,6 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 viewOffset = DrawOffset + drawOffsetNoise; - - - bool cursorOnOverlay = false; if (HighlightedLocation != null) { Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; @@ -574,52 +571,41 @@ namespace Barotrauma { locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight; } - - Rectangle highlightedLocationRect = new Rectangle(highlightedLocationDrawPos.ToPoint(), new Point(GUI.IntScale(25))); - Rectangle overlayRect = Rectangle.Union(highlightedLocationRect, locationInfoRt.Rect); - if (overlayRect.Contains(PlayerInput.MousePosition)) - { - cursorOnOverlay = true; - } locationInfoOverlay?.AddToGUIUpdateList(order: 1); } - if (!cursorOnOverlay) + float closestDist = 0.0f; + HighlightedLocation = null; + if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer)) { - float closestDist = 0.0f; - HighlightedLocation = null; - if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer)) + for (int i = 0; i < Locations.Count; i++) { - for (int i = 0; i < Locations.Count; i++) + Location location = Locations[i]; + if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } + + Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; + if (!rect.Contains(pos)) { continue; } + + Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; + float iconScale = generationParams.LocationIconSize / locationSprite.size.X; + if (location == currentDisplayLocation) { iconScale *= 1.2f; } + + Rectangle drawRect = locationSprite.SourceRect; + drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); + drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f); + drawRect.X = (int)pos.X - drawRect.Width / 2; + drawRect.Y = (int)pos.Y - drawRect.Width / 2; + + if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; } + + float dist = Vector2.Distance(PlayerInput.MousePosition, pos); + if (HighlightedLocation == null || dist < closestDist) { - Location location = Locations[i]; - if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } - - Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; - if (!rect.Contains(pos)) { continue; } - - Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; - float iconScale = generationParams.LocationIconSize / locationSprite.size.X; - if (location == currentDisplayLocation) { iconScale *= 1.2f; } - - Rectangle drawRect = locationSprite.SourceRect; - drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); - drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f); - drawRect.X = (int)pos.X - drawRect.Width / 2; - drawRect.Y = (int)pos.Y - drawRect.Width / 2; - - if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; } - - float dist = Vector2.Distance(PlayerInput.MousePosition, pos); - if (HighlightedLocation == null || dist < closestDist) - { - closestDist = dist; - HighlightedLocation = location; - } + closestDist = dist; + HighlightedLocation = location; } } } - if (SelectedConnection != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 10eb37bcd..99c42266e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -528,11 +528,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/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..58c9903e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -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) @@ -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; } 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/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/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index d30dee7fa..250ad651f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -474,7 +474,7 @@ namespace Barotrauma }; missionRewardTexts.Add(rewardText); - LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + LocalizedString reputationText = mission.GetReputationRewardText(); if (!reputationText.IsNullOrEmpty()) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 1d51cb8f1..254d89608 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 a68f55566..4053827f4 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/SlideshowPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs index 2990f151f..458614977 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs @@ -106,7 +106,7 @@ namespace Barotrauma { var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; currentText = slide.Text - .Replace("[submarine]", Submarine.MainSub?.Info.Name ?? "Unknown") + .Replace("[submarine]", Submarine.MainSub?.Info.Name ?? GameMain.GameSession?.SubmarineInfo?.Name ?? "Unknown") .Replace("[location]", Level.Loaded?.StartOutpost?.Info.Name ?? "Unknown"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index e8b831765..4b0ee066b 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) @@ -5646,8 +5634,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/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index ee18c028c..f53f49be0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -904,17 +904,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..ab4b38403 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; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 11a7a0f79..5337842fc 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.11.0.0 + 100.13.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 091940f14..f458b5f64 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.11.0.0 + 100.13.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index ae2aa79f3..eaf4449e0 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.11.0.0 + 100.13.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c89a589ad..5f96cbfd2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.11.0.0 + 100.13.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 355b769f7..b19369ff4 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.11.0.0 + 100.13.0.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..7588aaf7b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -302,12 +302,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 +401,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/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index c4a043bbd..353ef08d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -41,7 +41,7 @@ namespace Barotrauma clientsToRemove.Add(k); } } - if (!(clientsToRemove is null)) + if (clientsToRemove is not null) { foreach (var k in clientsToRemove) { @@ -62,7 +62,7 @@ namespace Barotrauma { foreach (Entity e in targets) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -85,7 +85,7 @@ namespace Barotrauma IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -149,5 +149,15 @@ namespace Barotrauma } GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } + + public void ServerWriteSelectedOption(Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION_SELECTED_OPTION); + outmsg.WriteUInt16(Identifier); + outmsg.WriteByte((byte)(selectedOption + 1)); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 5d9dde87c..62b7f9882 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -14,12 +14,12 @@ namespace Barotrauma foreach (Event ev in activeEvents) { - if (!(ev is ScriptedEvent scriptedEvent)) { continue; } + if (ev is not ScriptedEvent scriptedEvent) { continue; } var actions = FindActions(scriptedEvent); foreach (EventAction action in actions.Select(a => a.Item2)) { - if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; } + if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; } if (!convAction.TargetClients.Contains(sender)) { #if DEBUG || UNSTABLE @@ -42,6 +42,14 @@ namespace Barotrauma else { convAction.SelectedOption = selectedOption; + if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption)) + { + foreach (Client c in convAction.TargetClients) + { + if (c == sender) { continue; } + convAction.ServerWriteSelectedOption(c); + } + } } } return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/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/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 4cb7e4fd8..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 ?? 0); + 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 50eb92e20..e4cbbefcd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -349,15 +349,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 @@ -379,7 +373,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 5a2290a79..030c04129 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1745,9 +1745,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 +1758,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(); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 08aa0c92f..d03e7cda3 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.11.0.0 + 100.13.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -29,6 +29,7 @@ full true + TRACE;SERVER;WINDOWS;USE_STEAM 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 cdeff1678..bd7347e70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -352,6 +352,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 a31c36518..449ec68dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -354,6 +354,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; @@ -364,7 +368,7 @@ namespace Barotrauma { targetingTag = "husk"; } - else if (!Character.IsFriendly(targetCharacter)) + else if (!Character.IsSameSpeciesOrGroup(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { @@ -689,12 +693,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 824da423b..1a6ff9f52 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.IsPet || other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.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/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index fa297ba9e..1c6a1df7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -83,6 +83,7 @@ namespace Barotrauma { if (GameMain.GameSession.RoundDuration < 120.0f && speaker?.CurrentHull != null && + GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index c9f531f12..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++; } @@ -118,7 +118,11 @@ namespace Barotrauma Abandon = true; return; } - ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true); + ItemToContain = item ?? character.Inventory.FindItem(it => + CheckItem(it) && + //ignore items already in the container, unless we're trying to place to a specific slot, and the item's not in it + (it.Container != container.Item || (TargetSlot.HasValue && it.Container.OwnInventory.FindIndex(it) != TargetSlot)), + recursive: true); if (ItemToContain != null) { if (!character.CanInteractWith(ItemToContain, checkLinked: false)) 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 bfc1d97ec..587d61ce9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -412,7 +412,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)); } @@ -422,7 +423,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 a54535f6f..ebc92713c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -1023,7 +1023,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 @@ -1043,7 +1043,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 1f0ab15c8..cec3fcc4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -431,7 +431,7 @@ namespace Barotrauma } } - if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) + if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck) { Flip(); } @@ -1723,7 +1723,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 +1822,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 5aad72e9b..1d0f78157 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 @@ -881,7 +885,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(); @@ -1436,6 +1440,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 6fc41a1ac..d60b3c926 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -498,37 +498,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) @@ -538,18 +553,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); } } @@ -585,47 +612,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 6ee602141..61fa1af24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -365,9 +365,15 @@ namespace Barotrauma public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; + public bool IsHumanoid => Params.Humanoid; + + public bool IsMachine => Params.IsMachine; + public bool IsHusk => Params.Husk; public bool IsMale => info?.IsMale ?? false; @@ -1613,7 +1619,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); } - else if (humanPrefab.GiveItems(this, Submarine, spawnPoint)) + else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint)) { return; } @@ -1752,7 +1758,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(); @@ -3881,8 +3887,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); @@ -4481,7 +4487,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(); @@ -5197,15 +5206,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 || CharacterParams.CompareGroup(me.Group, other.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 || CharacterParams.CompareGroup(me.Group, other.Group); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 9eb073d9d..dde2b1b08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -779,9 +779,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/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 3ab1fe9a1..c5ee4f6a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -146,12 +146,6 @@ namespace Barotrauma { return minVitality; } - - if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions)) - { - return Math.Max(vitality, MinVitality + 1); - } - return vitality; } @@ -587,6 +581,15 @@ namespace Barotrauma } } + private void KillIfOutOfVitality() + { + if (Vitality <= MinVitality && + !Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions)) + { + Kill(); + } + } + private readonly static List afflictionsToRemove = new List(); private readonly static List> afflictionsToUpdate = new List>(); public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) @@ -611,7 +614,7 @@ namespace Barotrauma } CalculateVitality(); - if (Vitality <= MinVitality) { Kill(); } + KillIfOutOfVitality(); } public float GetLimbDamage(Limb limb, string afflictionType = null) @@ -729,10 +732,7 @@ namespace Barotrauma existingAffliction.Duration = existingAffliction.Prefab.Duration; if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } CalculateVitality(); - if (Vitality <= MinVitality) - { - Kill(); - } + KillIfOutOfVitality(); return; } @@ -746,10 +746,7 @@ namespace Barotrauma Character.HealthUpdateInterval = 0.0f; CalculateVitality(); - if (Vitality <= MinVitality) - { - Kill(); - } + KillIfOutOfVitality(); #if CLIENT if (OpenHealthWindow != this && limbHealth != null) { @@ -844,11 +841,7 @@ namespace Barotrauma } #endif CalculateVitality(); - - if (Vitality <= MinVitality) - { - Kill(); - } + KillIfOutOfVitality(); } } @@ -879,7 +872,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; @@ -1025,17 +1022,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 7e8075078..ab58d8faa 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/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/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 2f2ff2595..7a4ceeb07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; @@ -17,6 +18,9 @@ namespace Barotrauma.Abilities { if (!addingFirstTime) { return; } + // do not run client-side in multiplayer + if (GameMain.NetworkMember is { IsClient: true }) { return; } + JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); if (apprentice is null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 766b48ea3..bb6a2ac0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -49,6 +49,16 @@ namespace Barotrauma.Abilities break; } } + + switch (abilityEffectType) + { + case AbilityEffectType.OnDieToCharacter: + if (characterAbilities.Any(a => a.RequiresAlive)) + { + DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute."); + } + break; + } } public void ActivateAbilityGroup(bool addingFirstTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 81056e5eb..e4bb04ce9 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; 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 c05d9836f..8f29501bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1263,6 +1263,22 @@ namespace Barotrauma } #endif + commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) => + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + NewMessage("Reputation:"); + foreach (var faction in campaign.Factions) + { + NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}"); + } + } + else + { + ThrowError("Could not show reputation (no active campaign)."); + } + }, null)); + commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) => { if (GameMain.GameSession?.GameMode is CampaignMode campaign) @@ -1315,10 +1331,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 4484a0765..1f5ea9e81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -171,7 +171,7 @@ namespace Barotrauma PumpSpeed, PumpMaxFlow, ReactorMaxOutput, - ReactorFuelEfficiency, + ReactorFuelConsumption, DeconstructorSpeed, FabricationSpeed } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 1c5c8f0b5..a01b1abd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -200,7 +200,7 @@ namespace Barotrauma } } - private int[] GetEndingOptions() + public int[] GetEndingOptions() { List endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList(); if (!ContinueConversation) { endings.Add(-1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 48413e84c..bdbf2928c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -68,7 +68,7 @@ namespace Barotrauma var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable()); if (emptyLocation != null) { - emptyLocation.ChangeType(campaign, Barotrauma.LocationType.Prefabs[LocationTypes[0]]); + emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]); unlockLocation = emptyLocation; } } @@ -77,7 +77,7 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } else if (!MissionTag.IsEmpty) { @@ -89,8 +89,9 @@ namespace Barotrauma } if (unlockedMission != null) { + unlockedMission.OriginLocation = campaign.Map.CurrentLocation; campaign.Map.Discover(unlockLocation, checkTalents: false); - if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null) + if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null) { DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\"."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index a4838f3e5..2a07a41fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -12,6 +12,7 @@ namespace Barotrauma public enum NetworkEventType { CONVERSATION, + CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, UNLOCKPATH diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 85ad5a4ff..d89d3e054 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -229,9 +229,8 @@ namespace Barotrauma } bool requiresRescue = element.GetAttributeBool("requirerescue", false); - var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos, giveTags: true); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos); if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) { outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier); @@ -240,6 +239,7 @@ namespace Barotrauma outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag); } } + if (spawnPos is WayPoint wp) { spawnedCharacter.GiveIdCardTags(wp); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 5b1f68213..2350443aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -94,7 +94,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 49572092e..da43f1289 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -125,6 +125,12 @@ namespace Barotrauma public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; + /// + /// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission. + /// Defaults to Locations[0] + /// + public Location OriginLocation; + public readonly Location[] Locations; public int? Difficulty @@ -144,7 +150,7 @@ namespace Barotrauma } } - private List delayedTriggerEvents = new List(); + private readonly List delayedTriggerEvents = new List(); public Action OnMissionStateChanged; @@ -160,12 +166,13 @@ namespace Barotrauma Headers = prefab.Headers; var messages = prefab.Messages.ToArray(); + OriginLocation = locations[0]; Locations = locations; var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction)); if (endConditionElement != null) { - completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})"); + completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})"); } for (int n = 0; n < 2; n++) @@ -407,7 +414,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 @@ -436,7 +443,7 @@ namespace Barotrauma { if (reputationReward.Key == "location") { - Locations[0].Reputation?.AddReputation(reputationReward.Value); + OriginLocation.Reputation?.AddReputation(reputationReward.Value); } else { @@ -546,17 +553,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/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 06ab5566e..918ed8ad6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -290,6 +290,16 @@ namespace Barotrauma else if (!character.Info.StartItemsGiven) { character.GiveJobItems(mainSubWaypoints[i]); + foreach (Item item in character.Inventory.AllItems) + { + //if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in + //we don't want that in this case, the crew's cards shouldn't be submarine-specific + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.SubmarineSpecificID = 0; + } + } } if (character.Info.HealthData != null) { @@ -298,6 +308,7 @@ namespace Barotrauma character.LoadTalents(); + character.GiveIdCardTags(mainSubWaypoints[i]); character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 6ca24aab7..16934e1bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -14,8 +14,9 @@ namespace Barotrauma { } - public CampaignMetadata(XElement element) + public void Load(XElement element) { + data.Clear(); foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index f0fb1c47a..4053498ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -27,7 +27,7 @@ namespace Barotrauma /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents /// /// - public static FactionAffiliation GetPlayerAffiliationStatus(Identifier identifier, ImmutableHashSet? characterList = null) + public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet? characterList = null) { if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } @@ -37,23 +37,20 @@ namespace Barotrauma { if (character.Info is not { } info) { continue; } - foreach (Faction faction in factions) + foreach (Faction otherFaction in factions) { - Identifier factionIdentifier = faction.Prefab.Identifier; + Identifier factionIdentifier = otherFaction.Prefab.Identifier; if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f) { - return factionIdentifier == identifier + return factionIdentifier == faction.Prefab.Identifier ? FactionAffiliation.Positive : FactionAffiliation.Negative; } } } - return FactionAffiliation.Neutral; } - public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet? characterList = null) => GetPlayerAffiliationStatus(faction.Prefab.Identifier, characterList); - public override string ToString() { return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index dafea534f..47e20b5f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -46,7 +46,7 @@ namespace Barotrauma private List factions; public IReadOnlyList Factions => factions; - public CampaignMetadata CampaignMetadata; + public readonly CampaignMetadata CampaignMetadata; protected XElement petsElement; @@ -157,6 +157,7 @@ namespace Barotrauma CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); + CampaignMetadata = new CampaignMetadata(); Identifier messageIdentifier = new Identifier("money"); #if CLIENT @@ -675,9 +676,11 @@ namespace Barotrauma //TODO: ignore players who don't have the permission to trigger a transition between levels? var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer)); + CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1; + //allow leaving if inside an outpost, and the submarine is either docked to it or close enough - Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers); - Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers); + Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam); + Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam); int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); @@ -691,11 +694,11 @@ namespace Barotrauma return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd; - static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { if (Level.Loaded.StartOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -705,23 +708,23 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtStartExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } } - static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any()) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtEndExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -733,7 +736,7 @@ namespace Barotrauma if (Level.Loaded.EndOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -743,13 +746,13 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtEndExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -870,7 +873,7 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); + location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty); location.Reset(this); } Map.ClearLocationHistory(); 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/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index c9eb3dabf..9bfbd31c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -53,7 +54,7 @@ namespace Barotrauma } } - private bool ValidateFlag(NetFlags flag) + private static bool ValidateFlag(NetFlags flag) { if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } #if DEBUG @@ -105,7 +106,6 @@ namespace Barotrauma #endif } CampaignID = currentCampaignID; - CampaignMetadata = new CampaignMetadata(); UpgradeManager = new UpgradeManager(this); InitFactions(); } @@ -194,7 +194,7 @@ namespace Barotrauma } break; case "metadata": - CampaignMetadata = new CampaignMetadata(subElement); + CampaignMetadata.Load(subElement); break; case "upgrademanager": case "pendingupgrades": @@ -237,10 +237,8 @@ namespace Barotrauma }; } - CampaignMetadata ??= new CampaignMetadata(); UpgradeManager ??= new UpgradeManager(this); - InitFactions(); #if SERVER characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); 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 8e31e3387..d622fa6db 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 5999d3b94..9eb7e4464 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -365,7 +365,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; } @@ -480,11 +481,16 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } + if (OutsideSubmarineFixture != null) { OutsideSubmarineFixture.CollidesWith = Category.None; } - 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 d668f5d6b..5258d5359 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -215,6 +215,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 6806ae2ea..3b97b9368 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 bf6f9f052..7506c6f76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -849,7 +849,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/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index d76db314d..f91a7089d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -762,7 +762,6 @@ namespace Barotrauma.Items.Components var relatedItem = FindContainableItem(contained); if (relatedItem != null) { - if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } if (relatedItem.ItemPos.HasValue) { Vector2 pos = relatedItem.ItemPos.Value; 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 bb32fe84b..3ecd2752f 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 4c0e4336f..f8875bd78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -781,7 +781,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; } @@ -789,7 +789,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 }) { @@ -903,7 +909,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; } @@ -922,6 +928,8 @@ namespace Barotrauma.Items.Components else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem) { if (targetItem.Removed) { return false; } + //hit the external collider of an item (turret?) of the same sub -> ignore + if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; } if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent() != null) && targetItem.Condition > 0) { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); @@ -974,8 +982,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) { @@ -986,14 +994,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..3b79208c7 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 } @@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value, value ? lightBrightness : 0.0f); + SetLightSourceState(value); } } @@ -236,7 +236,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) @@ -248,6 +248,12 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { +#if CLIENT + if (item.HiddenInGame) + { + Light.Enabled = false; + } +#endif CheckIfNeedsUpdate(); } @@ -263,8 +269,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 +290,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,7 +307,7 @@ namespace Barotrauma.Items.Components PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) { - SetLightSourceState(false, 0.0f); + SetLightSourceState(false); return; } @@ -325,7 +332,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 +364,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 +382,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/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index d00eb625c..b8c60dd0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -478,7 +478,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 42c07c3b1..2ad916128 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; @@ -1584,7 +1585,7 @@ namespace Barotrauma tags.Add(newTag); } - public IEnumerable GetTags() + public IReadOnlyCollection GetTags() { return tags; } @@ -1753,15 +1754,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); @@ -1775,7 +1775,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); @@ -1803,8 +1803,9 @@ namespace Barotrauma } } - LastConditionChange = condition - prev; + LastConditionChange = condition - prevCondition; ConditionLastUpdated = Timing.TotalTime; + prevCondition = condition; static void flagChangedConnections(Dictionary connections) { @@ -2172,8 +2173,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; } } @@ -2726,7 +2728,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; } } @@ -2757,7 +2759,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; } } @@ -2796,8 +2798,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 }) { @@ -3463,7 +3465,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(); @@ -3537,15 +3539,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/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 8df7baf01..36d95d908 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -867,6 +867,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++) @@ -1112,6 +1113,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); @@ -3189,7 +3191,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; } @@ -3245,10 +3247,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()) { @@ -3301,10 +3302,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); @@ -3347,14 +3354,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/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index afa0af7a1..204bc79b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -161,7 +161,7 @@ namespace Barotrauma /// public LevelData(LocationConnection locationConnection) { - Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; + Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; Difficulty = locationConnection.Difficulty; @@ -194,9 +194,9 @@ namespace Barotrauma /// /// Instantiates level data using the properties of the location /// - public LevelData(Location location, float difficulty) + public LevelData(Location location, Map map, float difficulty) { - Seed = location.BaseName; + Seed = location.BaseName + map.Locations.IndexOf(location); Biome = location.Biome; Type = LevelType.Outpost; Difficulty = difficulty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 7ddfd6134..8116071ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -646,7 +646,7 @@ namespace Barotrauma } } - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default) + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -661,14 +661,29 @@ namespace Barotrauma lp.Type == type && (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); - matchingLevelParams = biome.IsEmpty - ? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)) - : matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome)); + if (biomeId.IsEmpty) + { + //we don't want end levels when generating a completely random level (e.g. in mission mode) + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)); + } + else + { + bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome; + if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId))) + { + //in the end biome, we must choose level parameters meant specifically for the end levels + matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + else + { + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + } if (!matchingLevelParams.Any()) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); - if (!biome.IsEmpty) + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); + if (!biomeId.IsEmpty) { //try to find params that at least have a suitable type matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type); @@ -682,7 +697,7 @@ namespace Barotrauma if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty)) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index c79b87509..333c19174 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -457,12 +457,14 @@ namespace Barotrauma private struct LoadedMission { public MissionPrefab MissionPrefab { get; } + public int OriginLocationIndex { get; } public int DestinationIndex { get; } public bool SelectedMission { get; } - public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission) + public LoadedMission(MissionPrefab prefab, int originLocationIndex, int destinationIndex, bool selectedMission) { MissionPrefab = prefab; + OriginLocationIndex = originLocationIndex; DestinationIndex = destinationIndex; SelectedMission = selectedMission; } @@ -663,9 +665,10 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(id)) { continue; } var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); if (prefab == null) { continue; } + var origin = childElement.GetAttributeInt("origin", -1); var destination = childElement.GetAttributeInt("destinationindex", -1); var selected = childElement.GetAttributeBool("selected", false); - loadedMissions.Add(new LoadedMission(prefab, destination, selected)); + loadedMissions.Add(new LoadedMission(prefab, origin, destination, selected)); } } } @@ -926,6 +929,10 @@ namespace Barotrauma destination = Connections.First().OtherLocation(this); } var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub); + if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count) + { + mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex]; + } availableMissions.Add(mission); if (loadedMission.SelectedMission) { selectedMissions.Add(mission); } } @@ -1520,10 +1527,12 @@ namespace Barotrauma foreach (Mission mission in missions) { var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this); - var i = map.Locations.IndexOf(location); + var destinationIndex = map.Locations.IndexOf(location); + var originIndex = map.Locations.IndexOf(mission.OriginLocation); missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), - new XAttribute("destinationindex", i), + new XAttribute("destinationindex", destinationIndex), + new XAttribute("origin", originIndex), new XAttribute("selected", selectedMissions.Contains(mission)))); } locationElement.Add(missionsElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 4ccf8d657..d2d93d4c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -167,7 +167,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,10 +188,9 @@ namespace Barotrauma { //backwards compatibility int endLocationIndex = element.GetAttributeInt("endlocation", -1); - if (endLocationIndex > 0 && endLocationIndex < Locations.Count) + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) { endLocations.Add(Locations[endLocationIndex]); - Locations[endLocationIndex].LevelData.ReassignGenerationParams(Seed); } else { @@ -203,7 +202,7 @@ namespace Barotrauma int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty()); foreach (int endLocationIndex in endLocationindices) { - if (endLocationIndex > 0 && endLocationIndex < Locations.Count) + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) { endLocations.Add(Locations[endLocationIndex]); } @@ -245,7 +244,7 @@ namespace Barotrauma { Biome = endLocations.First().Biome }; - newEndLocation.LevelData = new LevelData(newEndLocation, difficulty: 100.0f); + newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f); Locations.Add(newEndLocation); endLocations.Add(newEndLocation); } @@ -339,7 +338,7 @@ namespace Barotrauma { if (StartLocation != null) { - StartLocation.LevelData = new LevelData(StartLocation, 0); + StartLocation.LevelData = new LevelData(StartLocation, this, 0); } //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy @@ -701,7 +700,7 @@ namespace Barotrauma foreach (Location location in Locations) { - location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome)); if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); @@ -902,6 +901,7 @@ namespace Barotrauma { for (int i = 0; i < endLocations.Count; i++) { + endLocations[i].LevelData.ReassignGenerationParams(Seed); var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i); if (outpostParams != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index b51eb65f5..e127601df 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); @@ -543,12 +552,8 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) - { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - + if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } + PlacedModule newModule = null; //try appending to the current module if possible if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) @@ -569,6 +574,7 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition otherGapPosition in GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) { + if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); if (newModule != null) { @@ -617,6 +623,16 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); } + + static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule) + { + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; } + } + return false; + } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 31dd82776..da55e612d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1367,7 +1367,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; @@ -1438,64 +1438,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; - 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) @@ -1542,7 +1543,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 ce0310d10..9b7b4d176 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -116,7 +116,7 @@ namespace Barotrauma get { return submarine; } } - public SubmarineBody(Submarine sub, bool showWarningMessages = true) + public SubmarineBody(Submarine sub, bool showErrorMessages = true) { this.submarine = sub; @@ -126,9 +126,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/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/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 5cd9c0579..33f28c356 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(); @@ -903,6 +912,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 ed786bec6..514e207b8 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 f093b31e1..706b09a0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1773,12 +1773,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 1dca1dad8..9c31562db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -57,13 +57,12 @@ namespace Barotrauma if (characterList.Any()) { - if (location?.Reputation is { } reputation && Faction.GetPlayerAffiliationStatus(reputation.Identifier, characterList) is FactionAffiliation.Positive) + if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction, characterList) is FactionAffiliation.Positive) { price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); } price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); } - return (int)price; } } @@ -270,10 +269,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) { @@ -342,6 +390,7 @@ namespace Barotrauma private Dictionary targetProperties { get; } private readonly ImmutableArray MaxLevelsMods; + public readonly ImmutableHashSet ResourceCosts; public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { @@ -354,6 +403,7 @@ namespace Barotrauma var targetProperties = new Dictionary(); var maxLevels = new List(); + var resourceCosts = new HashSet(); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); if (!nameIdentifier.IsEmpty) @@ -396,6 +446,11 @@ namespace Barotrauma maxLevels.Add(new UpgradeMaxLevelMod(subElement)); break; } + case "resourcecost": + { + resourceCosts.Add(new UpgradeResourceCost(subElement)); + break; + } #if CLIENT case "decorativesprite": { @@ -427,6 +482,7 @@ namespace Barotrauma this.targetProperties = targetProperties; MaxLevelsMods = maxLevels.ToImmutableArray(); + ResourceCosts = resourceCosts.ToImmutableHashSet(); upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? .ToImmutableHashSet() ?? ImmutableHashSet.Empty; @@ -469,6 +525,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/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/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 36a6ddfa9..2193e4329 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,40 @@ +--------------------------------------------------------------------------------------------------------- +v100.13.0.0 +--------------------------------------------------------------------------------------------------------- + +- Fixes included in versions up to v0.21.0.0. +- Added translations for the endgame and faction content. +- Fixed reputation resetting between rounds. +- Added a round light component variant. +- Fixed z-fighting in HuskDistrict_Module_02. +- Fixed missing gaps in the Winterhalter wreck in the end level. +- Fixed end levels still sometimes including normal level geometry in saves started in the stable version. + +--------------------------------------------------------------------------------------------------------- +v100.12.0.0 +--------------------------------------------------------------------------------------------------------- + +- Fixes included in versions up to v0.20.15.0. +- Fixed faction-specific bots not getting the correct ID card tags if they spawn for the first time in an outpost (= if you save and quit and reload in an outpost). +- The "x reputation needed to unlock some biome" text is hidden in the tab menu if some other faction has already unlocked the path. +- Fixed NPC following the player indefinitely in the "way to ascension" event in which you need to deliver husk eggs. +- Fixed "Campaigning" talent not giving a discount on upgrades. +- Fixed items appearing stolen if you enter another outpost that happens to have the same name as the one you stole the items from. +- The generic outpost NPC greeting lines ("nice to see a friendly face", etc) aren't used when you have negative reputation. +- Fixed ladders being drawn in front of the docking hatch in DockingModule_01.. +- Fixed Subra assassination events working even if you've started the main husk cult chain (or even after you've hired Subra) +- Fixed submarine name being displayed as "Unknown" in the ending montage. +- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen. +- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub. +- Adjusted the layout of the research beacon station to prevent the entrance from ending up in an impossible-to-reach spot, made beacon station docking ports visible on sonar. +- Fixed missing gaps in the end level's wrecks. +- Fixed ancient weapons not damaging doors anymore. +- Reputation rewards are displayed in the round summary. +- Fixed missions that are configured to give location reputation (= reputation for the faction controlling the location you got the mission from) sometimes not giving rep. Happened because the game assumed the correct location is the one at the start of the connection the mission takes place in, which may not be the case (the location may have been unlocked further on the map, or it may be at the end of the connection). +- Fixed mini nuke explosion sound. +- Fixed "could not determine reputation change" console warnings when checking reputation in the tab menu or round summary during the 1st round in MP. +- Fixed campaign map sometimes displaying incorrect reputation values in multiplayer. + --------------------------------------------------------------------------------------------------------- v100.11.0.0 --------------------------------------------------------------------------------------------------------- @@ -120,6 +157,74 @@ Test version of the faction overhaul: - There's now always two paths from biome to another, one controlled by the Coalition and one by Separatists. - Improvements to the campaign map. +--------------------------------------------------------------------------------------------------------- +v0.21.0.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Changes: +- The "max missions" campaign setting is restricted to a maximum of 3. +- 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. + +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. +- 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. + +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, Projectile, and Throwable 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". + +--------------------------------------------------------------------------------------------------------- +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 +--------------------------------------------------------------------------------------------------------- + +- Fixed Revenge Squad talent doing nothing. +- Fixed Quickdraw talent's 8 second cooldown not working. +- Fixed flamer being fabricable without the appropriate talent. +- Fixed incorrect Aggressive Engineering talent description. + +--------------------------------------------------------------------------------------------------------- +v0.20.14.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed missing Rifle fabrication recipe. +- Fixed command devices (nav terminals, status monitors) not being in the electrical category, preventing "Better Than New" talent from having an effect on them. +- Fixed Graduation Ceremony still unlocking double the talents in multiplayer. +- Spawn husk eggs instead of husk syringes as creature loot. +- Cultist events require husk eggs instead of syringes. +- Adjusted shotgun fabrication recipe. + +--------------------------------------------------------------------------------------------------------- +v0.20.13.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed incorrect admin module sometimes being used in colonies, making it impossible to get to. +- Fixed a performance issue (character vitality logic being unnecessarily heavy) caused by some of the recent talent changes. +- Fixed inability to fabricate Rifle Bullets. +- Fixed contained items that have been configured to be hidden not being positioned correctly (meaning any positional effects done by the contained item would not work correctly). + --------------------------------------------------------------------------------------------------------- v0.20.12.0 --------------------------------------------------------------------------------------------------------- @@ -133,20 +238,19 @@ Talent overhaul: Balance: - Slightly adjusted values of handheld weapons. - - Power levels match cost better. - - Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls). - - Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though). - - Improved ammo availability for basic weapons. -- Made some weapons available later in game, to increase feeling of progression. + - Damage values of weapons have been adjusted to be more in line with their cost + - Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls). + - Improved ammo availability for basic weapons. + - Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. +- Made some weapons available later in the game to increase the feeling of progression. - Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses. - - Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances. - - PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths. - - Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair. - - All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit. -- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. + - Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances. + - PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths. + - Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair. + - All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit. - Chance of finding good/excellent/masterwork quality items in higher-difficulty levels. - Plasma cutter is now much better at cutting. -- Rebalanced damage dealt by tools. Damage should be a bit higher overall. +- Rebalanced damage dealt by tools. Damage should be a bit higher overall. (don't actually try to fight mudraptors with a wrench though) Tutorial improvements: - A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost. @@ -157,27 +261,27 @@ Tutorial improvements: Changes and additions: - New weapons: Rifle, Heavy Machine Gun, Machine Pistol, Harpoon Coil-Rifle. -- Flashlight can now be attached on all the ranged weapons held with two hands. +- Flashlight can now be attached to all ranged weapons held with two hands. - Limit which submarines are available in each outpost: high-tier subs become available as you get further in the campaign, and the submarine class selection depends on the type of the outpost. +- Two new music tracks. - Added a slider to the fabricator that can be used to select how many items to fabricate. - Added an option to hide enemy health bars. - Items' damage modifiers are shown in store tooltips. -- Added a button for treating all characters in one go to the medical clinic. +- Added a button for treating all characters in one go in the medical clinic. - Breaches through the submarine's outer hull throws shrapnels that can cause minor damage to nearby characters, making monsters that can't get inside more of a threat to the crew (as opposed to just the submarine itself). - Added a new honking scary random event to beacon stations. -- Added some particle, sound and light effects to water-sensitive materials and made them explode when they've been in water for 3 seconds, not immediately. +- Water-sensitive materials now explode when they've been in water for 3 seconds, not immediately. Added particle, sound and light effects. - Affliction descriptions change depending on the strength of the affliction, and whether you're treating someone else or yourself. - Added a button for opening the Steam Workshop to all tabs of the workshop menu. - Added tooltips that explain how the bot spawn modes work to the server lobby. - Added various new loot items to different creatures. -- Large monsters (Abyss monsters, Moloch, Watcher) drop items upon death. -- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version. +- Large monsters (Abyss monsters, Moloch, Watcher) now also drop items upon death. +- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version. Typically syringes are crafted, so finding a syringe on creatures felt a bit out of place. The eggs look as yucky as you’d expect. - Made saline significantly less effective as a treatment for bloodloss to make blood packs more useful. - Nerfed flak cannon's explosive ammo. -- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders: implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions. +- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders note: it’s implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions. - Allow putting medium items (e.g. storage container) in medical and toxic cabinets. - Some changes to wrecked item sprites (replacing the old low-res pictures with modified versions of the normal items' sprites). -- SMG can now be crafted. - Optimized the server lobby: there was an issue in the logic that updates the microphone icon that caused the game to check available audio devices every frame. - Optimized status monitors: previously some parts of their UI were always updated regardless if anyone is viewing the UI. @@ -189,7 +293,7 @@ Multiplayer: - Fixed inability to connect to IPv4 servers when IPv6 is disabled. - Fixed occasional crashes when shutting down a server (for example with the error messages "pipe is broken" or "ChildServerRelay readTask did not run to completion"). - Fixed "no core packages in the list of mods the server has enabled" error when trying to join a server that's using a different version of the core package you have enabled. -- Fixed "Input contains duplicate packages" error still occuring if you try to join a server that has empty content packages when you don't have those packages yourself. +- Fixed "Input contains duplicate packages" error still occurring if you try to join a server that has empty content packages when you don't have those packages yourself. - Fixed networking errors when the connection to the server is momentarily lost and then re-established. - Added a cooldown to client name changes to prevent using it for spamming. - Fixed bans issued with the "banaddress" command using a client's Steam ID not working. @@ -214,12 +318,12 @@ Bugfixes: - Fixed PUCS not beeping when you're underwater without a tank if you're inside a hull that has oxygen in it. - Fixed some issues in sonar AITargets which made monsters hear the sonar when they shouldn't: switching to passive would immediately make the current directional ping cover 360 degrees, and whether the ping was directional or not would actually depend on whether the previous ping was directional, not what the mode is now. - Fixed items getting autofilled into non-interactable containers in wrecks and outposts. -- Fixes to ID card tag issues in wrecks (prevented accessing the secure containers with the ID cards looted from the corpses). +- Fixed ID cards looted from the corpses of a wreck not giving access to the secure containers in the wreck. - Fixed verifying file integrity on Steam resetting the server settings file. - Fixed crashing if you try to open an access-restricted directory in the file selection dialog. - Fixed a typo in physicorium shell's damage config, causing it to not do bleeding damage. - Fixed money gain/lose popups no longer showing in the campaign. -- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. Caused e.g. drunkenness and bloodloss to never fully go away, causing issues with some talent effects. +- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. As a result e.g. drunkenness and bloodloss never fully went away, which caused issues with some talent effects. - Fixed bots always opening the door/hatch they're trying to repair. - Fixed power indicator not rotating with batteries. - Fixed lights on welding tools and plasma cutters emitting light the next round if the round ends while using them. @@ -227,8 +331,8 @@ Bugfixes: - Fixed Berilia's bottom EDC not being wired to a supercapacitor and a loose wire between the flak cannon and the right supercapacitor. - Fixed status effects targeting "NearbyCharacters" or "NearbyItems" being applied twice. Modders: if you used this, double the effects (e.g. damage) to get the same results as previously. - Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor". -- Fixed speech impediment from the husk infection making the bots unable to register any new targets autonomously (= without orders). -- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore any new enemies when they first envounter them. +- Fixed speech impediment from the husk infection making the bots unable to register any new targets on their own (= without being ordered). +- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore new enemies for a while when they first encounter them, unless being attacked. - Fixed the default aim assist being 50% instead of 5%. Fixed aim assist not resetting when the reset button is pressed on the settings window. - Fixed other players not seeing the spray particles when someone uses a sprayer in multiplayer. - Fixed ability to "fire" (just dropping the projectile) hardpoints that are connected to a periscope and loader. @@ -245,23 +349,23 @@ Bugfixes: - Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel. - Fixed loot sometimes spawning in vending machines' output slots. - Fixed water level sometimes "flickering" up and down when water is leaking to a room from the left or right. -- Fixed resetting UI position doing nothing to equipped items' UIs (e.g. handheld status monitor). +- Fixed resetting the UI position doing nothing to equipped items' UIs (e.g. handheld status monitor). - Fixed items equipped in the health interface slot being sellable. - Fixed inconsistent view ranges of large turrets. - Fixed SMG magazine shape being inconsistent with the shape of the mag well on the SMG sprite. - Fixed character portrait and health bar buttons being clickable (despite being hidden) when the health interface is open. -- Attempt to fix occasional crashes due to location store being null when teleporting from location to another with console commands. +- Fixed occasional crashes due to location store being null when teleporting from location to another with console commands. - Fixes to impact-sensitive items exploding at the start of the round (e.g. at the start of explosive transport missions or when purchasing explosives). -- Attempt to fix bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret. +- Fixed bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret. - Fixed focus staying on the highlighted item/character indefinitely if you keep holding LMB, even if you're outside interaction range. -- Prevented spawning of genetic materials outside creature inventories when the inventory size was too small, by increasing inventory sizes. +- Fixed some creatures not having enough space in their inventory for the genetic materials to spawn into. - Fixed minerals still sometimes being placed outside the level in mineral missions. -- Yet another fix to cave tunnels sometimes being too narrow to pass through. +- Fixed cave tunnels sometimes being too narrow to pass through. - Fixed "man and his raptor" outpost event giving 1000 marks in an incorrect branch of the dialog (the one where you immediately accept the NPC on board, instead of the one where the NPC says they'll pay you 1000 mk). - Fixed cases of interaction texts for focused item (most notoriously, the planter) not being updated correctly. - Fixed "snap to grid" causing door gaps to get misaligned. - Fixed weird equipping behavior on fruit and paints, causing them to be equipped in both hands when trying to unequip. -- Fixed junction boxes not getting damaged by water since the power rework. +- Fixed junction boxes not getting damaged by water. - Fixed opiate withdrawal only reducing down to 20%, but never fully healing by itself. - Fixed engines reverting back to the non-damaged sprite when they're damaged badly enough that the sprite starts shaking. - Fixed walls being set up incorrectly in vertical abandoned outpost hallway modules, causing them to stick out into the connected modules. @@ -272,16 +376,16 @@ Bugfixes: Modding: - Added a button to the main menu that can be used to update all installed mods when there's updates available. -- Mods with errors can no longer be enabled. +- Mods with errors can no longer be enabled. This change should encourage modders to fix errors in their mods and report bugs, as well as discourage players from ignoring errors. - Removed most of the debug console error spam seen when launching the game or opening the settings menu when faulty mods are installed. - Fixed mods failing to show up in the mods list at all when they have certain kinds of errors. -- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral: always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the requirements are matched. +- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral, meaning it always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the (skill) requirements are matched. - Fixed increasing an item's HealthMultiplier making the items appear damaged in existing subs/saves (e.g. if you doubled an item's maximum condition, the items would remain in the old maximum condition and appear 50% damaged). - Fixed crashing if a talent is triggered when the character receives some affliction, and that talent applies the same affliction on the character. - Fixed crashing if the ingredient of a fabrication recipe can't be found. - Fixed inability to sync properties of ItemComponents that the item has multiple of (meaning that it was only possible to e.g. edit the light color of the item's first LightComponent if it has multiple). -- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses). -- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration). +- Added 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses). +- Added 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration). - Added 'DontApplyToHands' property to Propulsion, preventing extra force applying to hands when the item is held in hands (instead applying only to the character's whole body). - Added a skill requirement conditional for StatusEffect, example: to make a status effect occur only if the target has less than 35 weapon skill. - Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. E.g. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5 s. @@ -290,8 +394,9 @@ Modding: - Added UseEnvironment.None to Propulsion component. - Fixed the debug console command "head" causing the character to disappear. The command can be used for changing the appearance of the character at runtime. - Status effects of type "OnUse" on projectiles now trigger when the projectile is launched. Previously it launched when the projectile hit the target. Use OnImpact (or OnSuccess/OnFailure) when you want something to happen when the projectile hits the target. -- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition off, because it'd override the affliction specific value. +- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition undefined, because it'd override the affliction specific value. - Fixed item's OnSpawn effects being applied twice. +- Fixed item components that inherit the status effects from another item component (e.g. the medical syringes) triggering the status effects twice when the effect is triggered via the item and not via the item component. Didn’t cause issues with the vanilla items, but might affect some mods. If your item e.g. suddenly does only half of the damage after the update, it’s possible that it was affected by the bug. Just double the effect to fix it. --------------------------------------------------------------------------------------------------------- v0.19.14.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