diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 9c1a5ee94..3d1153ba3 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,8 +73,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.10.7.2 (Autumn Update 2025 Hotfix 4) - - v1.11.0.0 (Unstable) + - v1.11.4.1 (Winter Update 2025) - Other validations: required: true diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index bdc1abf4b..49c4b4309 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -9,7 +9,7 @@ namespace Barotrauma { public override void DebugDraw(SpriteBatch spriteBatch) { - if (Character.IsUnconscious || !Character.Enabled || !Enabled) { return; } + if (Character.IsUnconscious || !Character.Enabled || !Enabled || Screen.Selected?.Cam is { Zoom: < 0.4f }) { return; } Vector2 pos = Character.DrawPosition; pos.Y = -pos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index c70862a41..4e628af8b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -11,7 +11,7 @@ namespace Barotrauma public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (Character == Character.Controlled) { return; } - if (!DebugAI) { return; } + if (!DebugAI || Screen.Selected?.Cam is { Zoom: < 0.4f }) { return; } Vector2 pos = Character.DrawPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 2d61423aa..6e2ac141b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -35,11 +35,7 @@ namespace Barotrauma CharacterStateInfo serverPos = character.MemState.Last(); if (!character.isSynced) { - SetPosition(serverPos.Position, lerp: false); - Collider.LinearVelocity = Vector2.Zero; - character.MemLocalState.Clear(); - character.LastNetworkUpdateID = serverPos.ID; - character.isSynced = true; + SyncPosition(serverPos); return; } @@ -198,11 +194,7 @@ namespace Barotrauma if (!character.isSynced) { - SetPosition(serverPos.Position, lerp: false); - Collider.LinearVelocity = Vector2.Zero; - character.MemLocalState.Clear(); - character.LastNetworkUpdateID = serverPos.ID; - character.isSynced = true; + SyncPosition(serverPos); return; } @@ -319,6 +311,15 @@ namespace Barotrauma if (character.MemLocalState.Count > 120) { character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120); } character.MemState.Clear(); } + + void SyncPosition(CharacterStateInfo serverPos) + { + SetPosition(serverPos.Position, lerp: false); + Collider.LinearVelocity = Vector2.Zero; + character.MemLocalState.Clear(); + character.LastNetworkUpdateID = serverPos.ID; + character.isSynced = true; + } } /// @@ -657,7 +658,7 @@ namespace Barotrauma public void DebugDraw(SpriteBatch spriteBatch) { - if (!GameMain.DebugDraw || !character.Enabled) { return; } + if (!GameMain.DebugDraw || !character.Enabled || Screen.Selected?.Cam is { Zoom: < 0.2f }) { return; } if (simplePhysicsEnabled) { return; } foreach (Limb limb in Limbs) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 7b92c12ac..0180efb8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -54,15 +54,29 @@ namespace Barotrauma get { return controlled; } set { - if (controlled == value) return; - if ((!(controlled is null)) && (!(Screen.Selected?.Cam is null)) && value is null) + if (controlled == value && controlled == null) { - Screen.Selected.Cam.TargetPos = Vector2.Zero; - Lights.LightManager.ViewTarget = null; + // Return early, but only when setting the controlled to null, because on controlling a character, we'll want to ensure that the target is both enabled and unfrozen. + return; + } + if (controlled != value) + { + CharacterHealth.OpenHealthWindow = null; + if (controlled != null && value == null) + { + if (Screen.Selected?.Cam is Camera camera) + { + camera.TargetPos = Vector2.Zero;; + } + Lights.LightManager.ViewTarget = null; + } } controlled = value; - if (controlled != null) controlled.Enabled = true; - CharacterHealth.OpenHealthWindow = null; + if (controlled != null) + { + controlled.Enabled = true; + controlled.AnimController.Frozen = false; + } } } @@ -442,7 +456,7 @@ namespace Barotrauma { cam.OffsetAmount = targetOffsetAmount = maxOffset; } - else if (SelectedItem != null && ViewTarget == null && + else if (SelectedItem != null && ViewTarget == null && !IsIncapacitated && SelectedItem.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this))) { cam.OffsetAmount = targetOffsetAmount = 0.0f; @@ -456,7 +470,7 @@ namespace Barotrauma } else if (Lights.LightManager.ViewTarget == this) { - if (GUI.PauseMenuOpen || IsUnconscious) + if (GUI.PauseMenuOpen || IsIncapacitated) { if (deltaTime > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 3b937d68a..9ba621c32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -23,8 +23,8 @@ namespace Barotrauma memState.Clear(); return; } - - //freeze AI characters if more than x seconds have passed since last update from the server + + //freeze other characters (than the controlled) if more than x seconds have passed since last update from the server if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - NetConfig.FreezeCharacterIfPositionDataMissingDelay) { AnimController.Frozen = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index c7bfa8924..9e38c0b43 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -104,6 +104,8 @@ namespace Barotrauma private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; + private LocalizedString prevHighlightedAfflictionDescription; + /// /// Timer for updating visuals (limb tints and overlays) caused by the affliction /// @@ -120,7 +122,7 @@ namespace Barotrauma private float updateDisplayedAfflictionsTimer; private const float UpdateDisplayedAfflictionsInterval = 0.5f; - private List currentDisplayedAfflictions = new List(); + private readonly List currentDisplayedAfflictions = new List(); public float DisplayedVitality, DisplayVitalityDelay; @@ -662,7 +664,8 @@ namespace Barotrauma else { forceAfflictionContainerUpdate = true; - currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); + currentDisplayedAfflictions.Clear(); + currentDisplayedAfflictions.AddRange(GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null)); currentDisplayedAfflictions.Sort((a1, a2) => { int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); @@ -1165,7 +1168,6 @@ namespace Barotrauma statusIconVisibleTime[afflictionPrefab] += deltaTime; Color color = GetAfflictionIconColor(afflictionPrefab, affliction); - var matchingIcon = afflictionIconContainer.GetChildByUserData(afflictionPrefab) ?? hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab); @@ -1177,9 +1179,20 @@ namespace Barotrauma ToolTip = $"‖color:{color.ToStringHex()}‖{affliction.Prefab.Name}‖color:end‖", CanBeSelected = false }; + new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true) + { + CanBeFocused = false + }; + } + if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay) + { + matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform; + } + else + { if (affliction.Prefab.ShowDescriptionInTooltip) { - matchingIcon.ToolTip = matchingIcon.ToolTip + "\n" + affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.Self); + matchingIcon.ToolTip = $"‖color:{color.ToStringHex()}‖{affliction.Prefab.Name}‖color:end‖" + "\n" + affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.Self); } if (affliction == pressureAffliction) { @@ -1191,14 +1204,6 @@ namespace Barotrauma } matchingIcon.ToolTip = RichString.Rich(matchingIcon.ToolTip); - new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true) - { - CanBeFocused = false - }; - } - if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay) - { - matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform; } var image = matchingIcon.GetChild(); image.Color = color; @@ -1574,12 +1579,14 @@ namespace Barotrauma CanBeFocused = false }; + prevHighlightedAfflictionDescription = affliction.Prefab.GetDescription( + affliction.Strength, + Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter); var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), - RichString.Rich(affliction.Prefab.GetDescription( - affliction.Strength, - Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter)), + RichString.Rich(prevHighlightedAfflictionDescription), textAlignment: Alignment.TopLeft, wrap: true) { + UserData = "description", CanBeFocused = false }; @@ -1724,7 +1731,6 @@ namespace Barotrauma var labelContainer = parent.GetChildByUserData("label"); var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock; - strengthText.Text = affliction.GetStrengthText(); strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, @@ -1743,6 +1749,19 @@ namespace Barotrauma vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); } + + var newDescription = + affliction.Prefab.GetDescription( + affliction.Strength, + Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter); + if (newDescription != prevHighlightedAfflictionDescription) + { + if (parent.GetChildByUserData("description") is GUITextBlock descriptionText) + { + descriptionText.Text = RichString.Rich(newDescription); + } + prevHighlightedAfflictionDescription = newDescription; + } } public bool OnItemDropped(Item item, bool ignoreMousePos) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index e69cbb887..14596349c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -1,78 +1,48 @@ -using Microsoft.Xna.Framework; +using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; namespace Barotrauma { - partial class MissionPrefab : PrefabWithUintIdentifier + internal sealed partial class MissionPrefab : PrefabWithUintIdentifier { - private ImmutableArray portraits = new ImmutableArray(); - + private ImmutableArray portraits = []; public bool HasPortraits => portraits.Length > 0; - public Sprite Icon - { - get; - private set; - } + public Sprite Icon { get; private set; } + public Color IconColor { get; private set; } - public Color IconColor - { - get; - private set; - } - - public bool DisplayTargetHudIcons - { - get; - private set; - } - - public float HudIconMaxDistance - { - get; - private set; - } - - public Sprite HudIcon - { - get - { - return hudIcon ?? Icon; - } - } - - public Color HudIconColor - { - get - { - return hudIconColor ?? IconColor; - } - } - public Color ProgressBarColor { get; private set; } + public bool DisplayTargetHudIcons { get; private set; } + public float HudIconMaxDistance { get; private set; } private Sprite hudIcon; + public Sprite HudIcon => hudIcon ?? Icon; + private Color? hudIconColor; + public Color HudIconColor => hudIconColor ?? IconColor; + + public Color ProgressBarColor { get; private set; } private ImmutableDictionary overrideMusicOnState; - partial void InitProjSpecific(ContentXElement element) + private void ParseConfigElementClient(ContentXElement element, MissionPrefab variantOf = null) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); - HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); - Dictionary overrideMusic = new Dictionary(); - List portraits = new List(); - foreach (var subElement in element.Elements()) + HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000f); + Dictionary overrideMusic = []; + List portraits = []; + foreach (ContentXElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "icon": - Icon = new Sprite(subElement); + Icon = new Sprite(subElement, GetTexturePath(subElement, variantOf)); IconColor = subElement.GetAttributeColor("color", Color.White); break; case "hudicon": - hudIcon = new Sprite(subElement); + hudIcon = new Sprite(subElement, GetTexturePath(subElement, variantOf)); hudIconColor = subElement.GetAttributeColor("color"); break; case "overridemusic": @@ -81,7 +51,7 @@ namespace Barotrauma subElement.GetAttributeIdentifier("type", Identifier.Empty)); break; case "portrait": - var portrait = new Sprite(subElement, lazyLoad: true); + Sprite portrait = new(subElement, GetTexturePath(subElement, variantOf), lazyLoad: true); if (portrait != null) { portraits.Add(portrait); @@ -89,7 +59,7 @@ namespace Barotrauma break; } } - this.portraits = portraits.ToImmutableArray(); + this.portraits = [.. portraits]; overrideMusicOnState = overrideMusic.ToImmutableDictionary(); ProgressBarColor = element.GetAttributeColor(nameof(ProgressBarColor), GUIStyle.Blue); } @@ -109,6 +79,11 @@ namespace Barotrauma return portraits[Math.Abs(randomSeed) % portraits.Length]; } + public string GetTexturePath(ContentXElement subElement, MissionPrefab variantOf = null) + => subElement.DoesAttributeReferenceFileNameAlone("texture") + ? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path) + : ""; + partial void DisposeProjectSpecific() { Icon?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 70b843e89..f6bce310a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -3338,7 +3338,13 @@ namespace Barotrauma if (!CanIssueOrders) { return false; } var character = userData as Character; int priority = GetManualOrderPriority(character, order); - SetCharacterOrder(character, order.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); + Item targetEntity = null; + if (order.MustSetTarget && order.TargetEntity == null) + { + var matchingItems = order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: characterContext ?? Character.Controlled); + targetEntity = matchingItems.FirstOrDefault(); + } + SetCharacterOrder(character, order.WithItemComponent(targetEntity).WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index c3dbabd08..d6e71e4b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -93,7 +93,7 @@ namespace Barotrauma public override void ShowStartMessage() { - foreach (Mission mission in Missions.ToList()) + foreach (Mission mission in Missions.OrderBy(m => m.Prefab.IsSideObjective).ToList()) { if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox( diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9d5e944fd..b2cbf69dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -112,8 +112,16 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(newCampaignButton.TextBlock, loadCampaignButton.TextBlock); - GameMain.NetLobbyScreen.CampaignSetupUI.StartNewGame = GameMain.Client.SetupNewCampaign; - GameMain.NetLobbyScreen.CampaignSetupUI.LoadGame = GameMain.Client.SetupLoadCampaign; + GameMain.NetLobbyScreen.CampaignSetupUI.StartNewGame = (SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings) => + { + GameMain.NetLobbyScreen.SetAFKSelected(false); + GameMain.Client.SetupNewCampaign(sub, saveName, mapSeed, settings); + }; + GameMain.NetLobbyScreen.CampaignSetupUI.LoadGame = (string filePath, Option backupIndex) => + { + GameMain.NetLobbyScreen.SetAFKSelected(false); + GameMain.Client.SetupLoadCampaign(filePath, backupIndex); + }; } partial void InitProjSpecific() @@ -169,6 +177,7 @@ namespace Barotrauma { StartRound = () => { + GameMain.NetLobbyScreen.SetAFKSelected(false); GameMain.Client.RequestStartRound(); } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 9dc938aa5..6151d06a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.Items.Components; namespace Barotrauma { @@ -22,6 +22,9 @@ namespace Barotrauma private GUIButton createEventButton; + public LevelGenerationParams BackgroundParams { get; private set; } + public Vector2 WaterParticleOffset; + public override void Start() { base.Start(); @@ -68,6 +71,12 @@ namespace Barotrauma } }; } + + if (Level.Loaded == null) + { + BackgroundParams ??= LevelGenerationParams.LevelParams.Where(lp => !lp.AllowedBiomeIdentifiers.Contains("endzone")).GetRandom(Rand.RandSync.Unsynced); + GameMain.LightManager.AmbientLight = BackgroundParams.AmbientLightColor; + } } public override void AddToGUIUpdateList() @@ -93,6 +102,8 @@ namespace Barotrauma sEvent.Update(deltaTime); } } + + BackgroundParams?.UpdateWaterParticleOffset(ref WaterParticleOffset, BackgroundParams.WaterParticleVelocity, deltaTime); } private void GenerateOutpost(Submarine submarine) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 5b95c0428..1f85da3c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -173,7 +173,8 @@ namespace Barotrauma List missionsToDisplay = new List(selectedMissions.Where(m => m.Prefab.ShowInMenus)); if (startLocation != null) { - foreach (Mission mission in startLocation.SelectedMissions) + //side objectives can't be selected manually (they're always selected), we need to add them separately from SelectedMissions + foreach (Mission mission in startLocation.SelectedMissions.Union(startLocation.AvailableMissions.Where(m => m.Prefab.IsSideObjective))) { if (missionsToDisplay.Contains(mission)) { continue; } if (!mission.Prefab.ShowInMenus) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index bcd08e213..8befbcd5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -119,8 +119,7 @@ namespace Barotrauma.Items.Components if (!attached) { Drop(false, null); - item.SetTransform(simPosition, 0.0f); - item.Submarine = sub; + item.SetTransform(simPosition, 0.0f, forceSubmarine: sub); AttachToWall(); PlaySound(ActionType.OnUse, attacher); ApplyStatusEffects(ActionType.OnUse, (float)Timing.Step, character: attacher, user: attacher); @@ -142,8 +141,7 @@ namespace Barotrauma.Items.Components } else { - item.SetTransform(simPosition, 0.0f); - item.Submarine = sub; + item.SetTransform(simPosition, 0.0f, forceSubmarine: sub); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 42bdee14d..a904e45c2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -28,8 +28,8 @@ namespace Barotrauma.Items.Components set { _chargeSoundWindupPitchSlide = new Vector2( - Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), - Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + MathHelper.Clamp(value.X, SoundChannel.MinFrequencyMultiplier, SoundChannel.MaxFrequencyMultiplier), + MathHelper.Clamp(value.Y, SoundChannel.MinFrequencyMultiplier, SoundChannel.MaxFrequencyMultiplier)); } } private Vector2 _chargeSoundWindupPitchSlide; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index cfd3d453b..0452c45e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -409,10 +409,10 @@ namespace Barotrauma.Items.Components loopingSoundChannel.Looping = true; loopingSoundChannel.Near = loopingSound.Range * 0.4f; loopingSoundChannel.Far = loopingSound.Range; - } - if (loopingSound.RoundSound.Stream) - { - loopingSoundChannel.StreamSeekPos = loopingSound.RoundSound.LastStreamSeekPos; + if (loopingSound.RoundSound.Stream) + { + loopingSoundChannel.StreamSeekPos = loopingSound.RoundSound.LastStreamSeekPos; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 68451d63e..b791fcf87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -434,12 +434,10 @@ namespace Barotrauma.Items.Components foreach (FabricationRecipe fi in fabricationRecipes.Values) { - RichString recipeTooltip = RichString.Rich(fi.TargetItem.Description); - if (fi.RequiresRecipe) - { - recipeTooltip += "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖"; - } - recipeTooltip = RichString.Rich(recipeTooltip); + RichString recipeTooltip = + fi.RequiresRecipe ? + RichString.Rich(fi.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖") : + RichString.Rich(fi.TargetItem.Description); var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) { @@ -894,10 +892,11 @@ namespace Barotrauma.Items.Components if (outputContainer.Inventory.IsEmpty()) { var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.Sprite; + Color iconColor = itemIcon == targetItem.TargetItem.Sprite ? targetItem.TargetItem.SpriteColor : targetItem.TargetItem.InventoryIconColor; itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), - color: Color.Lerp(targetItem.TargetItem.InventoryIconColor, Color.TransparentBlack, 0.5f), + color: Color.Lerp(iconColor, Color.TransparentBlack, 0.5f), scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y) * 0.9f); } } @@ -1132,24 +1131,27 @@ namespace Barotrauma.Items.Components if (!selectedRecipe.TargetItem.Description.IsNullOrEmpty()) { + RichString richDescription = RichString.Rich(selectedRecipe.TargetItem.Description); + var descriptionParent = largeUI ? paddedReqFrame : paddedFrame; var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionParent.RectTransform), - RichString.Rich(selectedRecipe.TargetItem.Description), + richDescription, font: GUIStyle.SmallFont, wrap: true); if (!largeUI) { description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); } - while (description.Rect.Height + nameBlock.Rect.Height > descriptionParent.Rect.Height) + while (description.Rect.Height + nameBlock.Rect.Height > descriptionParent.Rect.Height / 2) { var lines = description.WrappedText.Split('\n'); if (lines.Count <= 1) { break; } - var newString = string.Join('\n', lines.Take(lines.Count - 1)); + string newString = string.Join('\n', lines.Take(lines.Count - 1)); description.Text = newString.Substring(0, newString.Length - 4) + "..."; description.CalculateHeightFromText(); - description.ToolTip = selectedRecipe.TargetItem.Description; + description.ToolTip = richDescription; } + description.Text.RetrieveValue(); } IEnumerable inadequateSkills = Enumerable.Empty(); @@ -1328,7 +1330,10 @@ namespace Barotrauma.Items.Components var childContainer = child.GetChild(); childContainer.GetChild().TextColor = baseColor * (canBeFabricated ? 1.0f : 0.5f); - childContainer.GetChild().Color = recipe.TargetItem.InventoryIconColor * (canBeFabricated ? 1.0f : 0.5f); + + GUIImage icon = childContainer.GetChild(); + Color iconColor = icon.Sprite == recipe.TargetItem.Sprite ? recipe.TargetItem.SpriteColor : recipe.TargetItem.InventoryIconColor; + childContainer.GetChild().Color = iconColor * (canBeFabricated ? 1.0f : 0.5f); var limitReachedText = child.FindChild(nameof(FabricationLimitReachedText)); limitReachedText.Visible = !canBeFabricated && fabricationLimits.TryGetValue(recipe.RecipeHash, out int amount) && amount <= 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index e2afb09df..aea897a2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -137,46 +137,30 @@ namespace Barotrauma.Items.Components { foreach (var (position, emitter) in pumpOutEmitters) { - if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; } - //only emit "pump out" particles when underwater - Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad); - float angle = -item.RotationRad; - if (item.FlippedX) - { - relativeParticlePos.X = -relativeParticlePos.X; - angle += MathHelper.Pi; - } - if (item.FlippedY) - { - relativeParticlePos.Y = -relativeParticlePos.Y; - } - - emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle, - velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, -currFlow / maxFlow)); + if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; } + Emit(position, emitter); } } else if (currFlow > 0f) { foreach (var (position, emitter) in pumpInEmitters) { - Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad); - float angle = -item.RotationRad; - if (item.FlippedX) - { - relativeParticlePos.X = -relativeParticlePos.X; - angle += MathHelper.Pi; - } - if (item.FlippedY) - { - relativeParticlePos.Y = -relativeParticlePos.Y; - } - emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle, - velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, currFlow / maxFlow)); + Emit(position, emitter); } } + + void Emit(Vector2 position, ParticleEmitter emitter) + { + Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; + if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; } + if (item.FlippedY) { relativeParticlePos.Y = -relativeParticlePos.Y; } + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, -item.RotationRad); + float angle = -item.RotationRad; + if (item.FlippedX) { angle += MathHelper.Pi; } + emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle, + velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, currFlow / maxFlow), mirrorAngle: item.FlippedX ^ item.FlippedY); + } } private float flickerTimer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 9129002e4..872b9be0c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1406,7 +1406,8 @@ namespace Barotrauma.Items.Components public void RegisterExplosion(Explosion explosion, Vector2 worldPosition) { - if (Character.Controlled?.SelectedItem != item) { return; } + if (Character.Controlled?.SelectedItem == null) { return; } + if (Character.Controlled.SelectedItem != Item && !Character.Controlled.SelectedItem.linkedTo.Contains(Item)) { return; } if (explosion.Attack.StructureDamage <= 0 && explosion.Attack.ItemDamage <= 0 && explosion.EmpStrength <= 0) { return; } Vector2 transducerCenter = GetTransducerPos(); if (Vector2.DistanceSquared(worldPosition, transducerCenter) > range * range) { return; } @@ -1564,7 +1565,15 @@ namespace Barotrauma.Items.Components foreach (Item item in Item.SonarVisibleItems) { System.Diagnostics.Debug.Assert(item.Prefab.SonarSize > 0.0f); - if (item.CurrentHull == null) + + if (item.CurrentHull != null) { continue; } + + bool isItemVisible = + item.ParentInventory == null || + item.GetComponent() is { IsActive: true } || + item.Container?.GetComponent() is { HideItems: false }; + + if (isItemVisible) { float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared(); if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 7a405bd47..910ef210e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components int connectorIntervalLeft = GetConnectorIntervalLeft(height, panel); int connectorIntervalRight = GetConnectorIntervalRight(height, panel); - foreach (Connection c in panel.Connections) + foreach (Connection c in panel.Connections.OrderBy(static c => c.DisplayOrder)) { //if dragging a wire, let the Inventory know so that the wire can be //dropped or dragged from the panel to the players inventory @@ -192,7 +192,7 @@ namespace Barotrauma.Items.Components { DrawWire(spriteBatch, equippedWire, new Vector2(x + width / 2, y + height - 150 * GUI.Scale), new Vector2(x + width / 2, y + height), - null, panel, ""); + null, panel, label: GetWireLabel(connection: null, wire: equippedWire)); if (DraggingConnected == equippedWire) { @@ -209,13 +209,9 @@ namespace Barotrauma.Items.Components { if (wire == DraggingConnected && mouseInRect) { continue; } if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } - - Connection recipient = wire.OtherConnection(null); - LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; - if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } DrawWire(spriteBatch, wire, new Vector2(x, y + height - 100 * GUI.Scale), new Vector2(x, y + height), - null, panel, label); + null, panel, label: GetWireLabel(connection: null, wire)); x += (int)step; } @@ -290,21 +286,7 @@ namespace Barotrauma.Items.Components { if (wire.Hidden || (DraggingConnected == wire && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } - - Connection recipient = wire.OtherConnection(this); - LocalizedString label; - if (wire.Item.IsLayerHidden) - { - label = TextManager.Get("ConnectionLocked"); - } - else - { - label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; - if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } - } - - DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label); - + DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label: GetWireLabel(connection: this, wire: wire)); wirePosition.Y += wireInterval; } @@ -362,6 +344,22 @@ namespace Barotrauma.Items.Components } } + private static LocalizedString GetWireLabel(Connection connection, Wire wire) + { + Connection recipient = wire.OtherConnection(connection); + LocalizedString label; + if (wire.Item.IsLayerHidden) + { + label = TextManager.Get("ConnectionLocked"); + } + else + { + label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; + if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } + } + return label; + } + public void Flash(Color? color = null, float flashDuration = 1.5f) { FlashTimer = flashDuration; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionSelectorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionSelectorComponent.cs new file mode 100644 index 000000000..378dd07e6 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionSelectorComponent.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class ConnectionSelectorComponent : ItemComponent + { + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + SelectedConnection = msg.ReadRangedInteger(0, 255); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 7a96851e2..eb371af3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -733,7 +733,7 @@ namespace Barotrauma { updateableComponents.Add(ic); } - isActive = true; + IsActive = true; } } @@ -2170,7 +2170,7 @@ namespace Barotrauma return; } - isActive = true; + IsActive = true; if (positionBuffer.Count > 0) { @@ -2515,8 +2515,7 @@ namespace Barotrauma { inventory.TryPutItem(item, user: null, allowedSlots: item.AllowedSlots, createNetworkEvent: false); } - item.SetTransform(inventory.Owner.SimPosition, 0.0f); - item.Submarine = inventory.Owner.Submarine; + item.SetTransform(inventory.Owner.SimPosition, 0.0f, forceSubmarine: inventory.Owner.Submarine); if (inventory.Owner is Character { Enabled: false } && item.body != null) { item.body.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 09e467505..129ef0958 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -230,7 +230,12 @@ namespace Barotrauma if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld && !doubleClicked && !secondaryDoubleClicked) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - Hull hull = FindHull(position); + Hull hull = + Screen.Selected is { IsEditor: true } ? + //use the unoptimized version in the editor to make sure newly placed hulls are found + //(FindHull uses the "EntityGrid" which is generated after loading the sub) + FindHullUnoptimized(position) : + FindHull(position); if (hull == null || hull.IdFreed) { return; } if (EditWater) @@ -361,7 +366,7 @@ namespace Barotrauma new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), 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) + if (GameMain.DebugDraw && Screen.Selected?.Cam is { Zoom: > 0.5f }) { GUIStyle.SmallFont.DrawString(spriteBatch, "Pressure: " + ((int)pressure - rect.Y).ToString() + " - Oxygen: " + ((int)OxygenPercentage), new Vector2(drawRect.X + 5, -drawRect.Y + 5), Color.White); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelGenerationParameters.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelGenerationParameters.cs new file mode 100644 index 000000000..121a79947 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelGenerationParameters.cs @@ -0,0 +1,74 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +namespace Barotrauma; + +internal partial class LevelGenerationParams : PrefabWithUintIdentifier +{ + /// Doesn't call SpriteBatch.Begin and SpriteBatch.End; They must be called manually. + public void DrawBackgrounds(SpriteBatch spriteBatch, Camera cam) + { + if (BackgroundTopSprite == null) { return; } + + Vector2 backgroundPos = cam.WorldViewCenter.FlipY() * 0.05f; + int backgroundSize = (int)BackgroundTopSprite.size.Y; + if (backgroundPos.Y >= backgroundSize) { return; } + + if (backgroundPos.Y < 0f) + { + BackgroundTopSprite.SourceRect = new Rectangle((int)backgroundPos.X, (int)backgroundPos.Y, backgroundSize, (int)Math.Min(-backgroundPos.Y, backgroundSize)); + BackgroundTopSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, Math.Min(-backgroundPos.Y, GameMain.GraphicsHeight)), + color: BackgroundTextureColor); + } + if (-backgroundPos.Y < GameMain.GraphicsHeight && BackgroundSprite != null) + { + BackgroundSprite.SourceRect = new Rectangle((int)backgroundPos.X, (int)Math.Max(backgroundPos.Y, 0), backgroundSize, backgroundSize); + BackgroundSprite.DrawTiled(spriteBatch, (backgroundPos.Y < 0f) ? new Vector2(0f, (int)-backgroundPos.Y) : Vector2.Zero, + new Vector2(GameMain.GraphicsWidth, (int)Math.Min(Math.Ceiling(backgroundSize - backgroundPos.Y), backgroundSize)), + color: BackgroundTextureColor); + } + } + + /// Doesn't call SpriteBatch.Begin and SpriteBatch.End; They must be called manually. + public void DrawWaterParticles(SpriteBatch spriteBatch, Camera cam, Vector2 offset) + { + if (WaterParticles == null || cam.Zoom <= 0.05f) { return; } + + float textureScale = WaterParticleScale; + Vector2 textureSize = new Vector2(WaterParticles.Texture.Width, WaterParticles.Texture.Height); + Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y); + offset -= origin; + + // Draw 4 layers of particles. + for (int i = 0; i < 4; i++) + { + float scale = 1f - i * 0.2f; + float alpha = MathUtils.InverseLerp(0.05f, 0.1f, cam.Zoom * scale); + if (alpha == 0f) { continue; } + + Vector2 newOffset = offset * scale; + newOffset += cam.WorldView.Size.ToVector2() * (1f - scale) * 0.5f; + newOffset -= new Vector2(256f * i); + + float newTextureScale = scale * textureScale; + + Vector2 newSize = textureSize * scale; + while (newOffset.X <= -newSize.X) { newOffset.X += newSize.X; } + while (newOffset.X > 0f) { newOffset.X -= newSize.X; } + while (newOffset.Y <= -newSize.Y) { newOffset.Y += newSize.Y; } + while (newOffset.Y > 0f) { newOffset.Y -= newSize.Y; } + + WaterParticles.DrawTiled(spriteBatch, origin + newOffset, cam.WorldView.Size.ToVector2() - newOffset, + color: WaterParticleColor * alpha, textureScale: new Vector2(newTextureScale)); + } + } + + public void UpdateWaterParticleOffset(ref Vector2 offset, Vector2 velocity, float deltaTime) + { + if (WaterParticles == null) { return; } + Vector2 waterTextureSize = WaterParticles.size * WaterParticleScale; + offset += velocity.FlipY() * WaterParticleScale * deltaTime; + offset.X %= waterTextureSize.X; + offset.Y %= waterTextureSize.Y; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 02a3d3834..a7c47f066 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -234,15 +234,7 @@ namespace Barotrauma WaterRenderer.Instance?.ScrollWater(waterParticleVelocity, deltaTime); - if (level.GenerationParams.WaterParticles != null) - { - Vector2 waterTextureSize = level.GenerationParams.WaterParticles.size * level.GenerationParams.WaterParticleScale; - waterParticleOffset += new Vector2(waterParticleVelocity.X, -waterParticleVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; - while (waterParticleOffset.X <= -waterTextureSize.X) { waterParticleOffset.X += waterTextureSize.X; } - while (waterParticleOffset.X >= waterTextureSize.X){ waterParticleOffset.X -= waterTextureSize.X; } - while (waterParticleOffset.Y <= -waterTextureSize.Y) { waterParticleOffset.Y += waterTextureSize.Y; } - while (waterParticleOffset.Y >= waterTextureSize.Y) { waterParticleOffset.Y -= waterTextureSize.Y; } - } + level.GenerationParams.UpdateWaterParticleOffset(ref waterParticleOffset, waterParticleVelocity, deltaTime); } public static VertexPositionColorTexture[] GetColoredVertices(VertexPositionTexture[] vertices, Color color) @@ -274,36 +266,7 @@ namespace Barotrauma ParticleManager particleManager = null) { spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap); - - Vector2 backgroundPos = cam.WorldViewCenter; - - backgroundPos.Y = -backgroundPos.Y; - backgroundPos *= 0.05f; - - if (level.GenerationParams.BackgroundTopSprite != null) - { - int backgroundSize = (int)level.GenerationParams.BackgroundTopSprite.size.Y; - if (backgroundPos.Y < backgroundSize) - { - if (backgroundPos.Y < 0) - { - var backgroundTop = level.GenerationParams.BackgroundTopSprite; - backgroundTop.SourceRect = new Rectangle((int)backgroundPos.X, (int)backgroundPos.Y, backgroundSize, (int)Math.Min(-backgroundPos.Y, backgroundSize)); - backgroundTop.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, Math.Min(-backgroundPos.Y, GameMain.GraphicsHeight)), - color: level.BackgroundTextureColor); - } - if (-backgroundPos.Y < GameMain.GraphicsHeight && level.GenerationParams.BackgroundSprite != null) - { - var background = level.GenerationParams.BackgroundSprite; - background.SourceRect = new Rectangle((int)backgroundPos.X, (int)Math.Max(backgroundPos.Y, 0), backgroundSize, backgroundSize); - background.DrawTiled(spriteBatch, - (backgroundPos.Y < 0) ? new Vector2(0.0f, (int)-backgroundPos.Y) : Vector2.Zero, - new Vector2(GameMain.GraphicsWidth, (int)Math.Min(Math.Ceiling(backgroundSize - backgroundPos.Y), backgroundSize)), - color: level.BackgroundTextureColor); - } - } - } - + level.GenerationParams.DrawBackgrounds(spriteBatch, cam); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, @@ -317,42 +280,7 @@ namespace Barotrauma backgroundCreatureManager?.Draw(spriteBatch, cam); } - if (level.GenerationParams.WaterParticles != null && cam.Zoom > 0.05f) - { - float textureScale = level.GenerationParams.WaterParticleScale; - - Rectangle srcRect = new Rectangle(0, 0, 2048, 2048); - Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y); - Vector2 offset = -origin + waterParticleOffset; - while (offset.X <= -srcRect.Width * textureScale) offset.X += srcRect.Width * textureScale; - while (offset.X > 0.0f) offset.X -= srcRect.Width * textureScale; - while (offset.Y <= -srcRect.Height * textureScale) offset.Y += srcRect.Height * textureScale; - while (offset.Y > 0.0f) offset.Y -= srcRect.Height * textureScale; - for (int i = 0; i < 4; i++) - { - float scale = (1.0f - i * 0.2f); - - //alpha goes from 1.0 to 0.0 when scale is in the range of 0.1 - 0.05 - float alpha = (cam.Zoom * scale) < 0.1f ? (cam.Zoom * scale - 0.05f) * 20.0f : 1.0f; - if (alpha <= 0.0f) continue; - - Vector2 offsetS = offset * scale - + new Vector2(cam.WorldView.Width, cam.WorldView.Height) * (1.0f - scale) * 0.5f - - new Vector2(256.0f * i); - - float texScale = scale * textureScale; - - while (offsetS.X <= -srcRect.Width * texScale) offsetS.X += srcRect.Width * texScale; - while (offsetS.X > 0.0f) offsetS.X -= srcRect.Width * texScale; - while (offsetS.Y <= -srcRect.Height * texScale) offsetS.Y += srcRect.Height * texScale; - while (offsetS.Y > 0.0f) offsetS.Y -= srcRect.Height * texScale; - - level.GenerationParams.WaterParticles.DrawTiled( - spriteBatch, origin + offsetS, - new Vector2(cam.WorldView.Width - offsetS.X, cam.WorldView.Height - offsetS.Y), - color: level.GenerationParams.WaterParticleColor * alpha, textureScale: new Vector2(texScale)); - } - } + level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset); GameMain.ParticleManager?.Draw(spriteBatch, inWater: true, inSub: false, ParticleBlendState.AlphaBlend, background: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index e37bd5193..e54f37f21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Lights } private float pulseAmount; - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates. 0 = not at all, 1 = alternates between full brightness and off.")] public float PulseAmount { get { return pulseAmount; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 5fe6c6acf..8e1b13ba1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -397,7 +397,7 @@ namespace Barotrauma } if (!description.IsNullOrEmpty()) { - CreateTextWithIcon(description, locationTypeToDisplay.Sprite); + CreateTextWithIcon(description, iconSprite: locationTypeToDisplay.Sprite); } int highestSubTier = location.HighestSubmarineTierAvailable(); @@ -417,32 +417,29 @@ namespace Barotrauma } if (highestSubTier > 0) { - CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), iconStyle: "LocationOverlaySubmarineIcon"); } if (overrideTiers != null) { foreach (var (subClass, tier) in overrideTiers) { - CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), iconStyle: "LocationOverlaySubmarineIcon"); } } CreateSpacing(10); - void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null) + void CreateTextWithIcon(LocalizedString text, Sprite iconSprite = null, string iconStyle = null) { - var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true) - { - Stretch = true, - CanBeFocused = true - }; - var guiIcon = - style == null ? - new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) : - new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style); - var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text); - textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0); - textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0); + GUITextBlock textBox = new(new RectTransform(new Vector2(0.9f, 0f), content.RectTransform), text, wrap: true); + + if (iconSprite == null && iconStyle == null) { return; } + float iconSize = GUIStyle.Font.LineHeight * 1.5f; + + textBox.Padding = textBox.Padding with { X = iconSize + 5f }; + RectTransform iconTF = new(new Point((int)iconSize), textBox.RectTransform, Anchor.CenterLeft) { IsFixedSize = true }; + if (iconSprite != null) { new GUIImage(iconTF, iconSprite, scaleToFit: true); } + if (iconStyle != null) { new GUIImage(iconTF, iconStyle, scaleToFit: true); } } void CreateSpacing(int height) @@ -486,10 +483,11 @@ namespace Barotrauma CreateSpacing(20); } - locationInfoOverlay.RectTransform.NonScaledSize = - new Point( - Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)), - (int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y)); + float childWidth = Math.Max(locationInfoOverlay.Rect.Width, content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X + textBlock.Padding.X + textBlock.Padding.Z : c.RectTransform.MinSize.X)); + childWidth = Math.Max(locationInfoOverlay.Rect.Width, childWidth); + float childHeight = content.Children.Sum(c => c.Rect.Height); + Vector2 childSize = new Vector2(childWidth, childHeight) / content.RectTransform.RelativeSize; + locationInfoOverlay.RectTransform.NonScaledSize = childSize.ToPoint(); } partial void ClearAnimQueue() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index f89edd996..65363372a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -34,6 +34,8 @@ namespace Barotrauma //which entities have been selected for editing public static HashSet SelectedList { get; private set; } = new HashSet(); + private static List oldRects = new List(); + private static Vector2 entityMovementNudge; public static List CopiedList = new List(); @@ -267,7 +269,7 @@ namespace Barotrauma i++; } highlightedEntities.Insert(i, e); - if (i == 0) highLightedEntity = e; + if (i == 0) { highLightedEntity = e; } } } UpdateHighlighting(highlightedEntities); @@ -278,10 +280,19 @@ namespace Barotrauma if (GUI.KeyboardDispatcher.Subscriber == null) { - Vector2 nudge = GetNudgeAmount(); - if (nudge != Vector2.Zero) + Vector2 previousNudge = entityMovementNudge; + entityMovementNudge = GetNudgeAmount(); + if (entityMovementNudge != Vector2.Zero) { - foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(nudge); } + if (previousNudge == Vector2.Zero) + { + oldRects = SelectedList.Select(entity => entity.Rect).ToList(); + } + foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(entityMovementNudge); } + } + else if (previousNudge != Vector2.Zero) + { + SubEditorScreen.StoreCommand(new TransformCommand(new List(SelectedList), SelectedList.Select(entity => entity.Rect).ToList(), oldRects, resized: false)); } } else @@ -352,7 +363,7 @@ namespace Barotrauma } } - SubEditorScreen.StoreCommand(new TransformCommand(new List(SelectedList),SelectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); + SubEditorScreen.StoreCommand(new TransformCommand(new List(SelectedList), SelectedList.Select(entity => entity.Rect).ToList(), oldRects, resized: false)); if (deposited.Any() && deposited.Any(entity => entity is Item)) { var depositedItems = deposited.Where(entity => entity is Item).Cast().ToList(); @@ -508,7 +519,7 @@ namespace Barotrauma selectionSize = Vector2.Zero; selectionPos = Vector2.Zero; } - + public static Vector2 GetNudgeAmount(bool doHold = true) { Vector2 nudgeAmount = Vector2.Zero; @@ -532,10 +543,10 @@ namespace Barotrauma } } - if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; - if (PlayerInput.KeyHit(Keys.Down)) nudgeAmount.Y = -1f; - if (PlayerInput.KeyHit(Keys.Left)) nudgeAmount.X = -1f; - if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; + if (PlayerInput.KeyHit(Keys.Up)) { nudgeAmount.Y = 1f; } + if (PlayerInput.KeyHit(Keys.Down)) { nudgeAmount.Y = -1f; } + if (PlayerInput.KeyHit(Keys.Left)) { nudgeAmount.X = -1f;} + if (PlayerInput.KeyHit(Keys.Right)) { nudgeAmount.X = 1f; } return nudgeAmount; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index c8c38d34a..4b3b70114 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -172,16 +172,19 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, drawPos - ExitPointSize.ToVector2() / 2, ExitPointSize.ToVector2(), Color.Cyan, thickness: 5); } - GUIStyle.SmallFont.DrawString(spriteBatch, - ID.ToString(), - new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), - color); - if (Tunnel?.Type != null) + if (Screen.Selected?.Cam is { Zoom: > 0.4f }) { GUIStyle.SmallFont.DrawString(spriteBatch, - Tunnel.Type.ToString(), - new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45), - color); + ID.ToString(), + new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), + color); + if (Tunnel?.Type != null) + { + GUIStyle.SmallFont.DrawString(spriteBatch, + Tunnel.Type.ToString(), + new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45), + color); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index c47244bf5..9c4efaaf3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -239,6 +239,13 @@ namespace Barotrauma.Networking } return; } + + //if we're still downloading mods, we're not ready to receive the campaign save + if (fileType == (byte)FileTransferType.CampaignSave && Screen.Selected is ModDownloadScreen) + { + GameMain.Client.CancelFileTransfer(transferId); + return; + } if (!ValidateInitialData(fileType, fileName, fileSize, out string errorMsg)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 528a32f6b..c416d7a10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -562,6 +562,11 @@ namespace Barotrauma.Networking { SendLobbyUpdate(); } + if (Timing.TotalTime > LastMissingCampaignSubRequestTime) + { + TryRequestMissingCampaignSubs(); + LastMissingCampaignSubRequestTime = Timing.TotalTime + MissingCampaignSubRequestInterval; + } } if (ServerSettings.VoiceChatEnabled) @@ -2284,13 +2289,7 @@ namespace Barotrauma.Networking if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); } } - if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) - { - foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name))) - { - GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign); - } - } + TryRequestMissingCampaignSubs(); GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating); GameMain.NetLobbyScreen.SetAllowAFK(allowAFK); @@ -2646,6 +2645,21 @@ namespace Barotrauma.Networking ClientPeer?.Send(msg, DeliveryMethod.Reliable); } + private double LastMissingCampaignSubRequestTime; + + const double MissingCampaignSubRequestInterval = 10.0f; + + private void TryRequestMissingCampaignSubs() + { + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) + { + foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name))) + { + GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign); + } + } + } + public void RequestFile(FileTransferType fileType, string file, string fileHash) { DebugConsole.Log( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs index 7cccef8c1..9073abf53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -91,7 +91,7 @@ namespace Barotrauma.Networking }); initializationStep = ConnectionInitialization.AuthInfoAndVersion; - timeout = NetworkConnection.TimeoutThreshold; + timeout = NetworkConnection.TimeoutThresholdNotInGame; heartbeatTimer = 1.0; isActive = true; @@ -139,7 +139,7 @@ namespace Barotrauma.Networking timeout = Screen.Selected == GameMain.GameScreen ? NetworkConnection.TimeoutThresholdInGame - : NetworkConnection.TimeoutThreshold; + : NetworkConnection.TimeoutThresholdNotInGame; var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 6f1f6fb86..1f1d74704 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -354,9 +354,11 @@ namespace Barotrauma { List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).Where(m => m.Prefab.ShowInMenus || GameMain.DebugDraw).ToList(); - if (!availableMissions.Any()) { availableMissions.Insert(0, null); } + if (availableMissions.None()) { availableMissions.Insert(0, null); } availableMissions.AddRange(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])); + //show side objectives last + availableMissions.Sort((m1, m2) => (m1?.Prefab.IsSideObjective ?? false).CompareTo(m2?.Prefab.IsSideObjective ?? false)); missionList.Content.ClearChildren(); @@ -390,6 +392,10 @@ namespace Barotrauma }; LocalizedString missionName = mission?.Name ?? TextManager.Get("NoMission"); + if (mission is { Prefab.IsSideObjective: true }) + { + missionName = TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), missionName); + } if (GameMain.DebugDraw && mission != null) { if (!mission.Prefab.ShowInMenus) { missionName = $"[HIDDEN] {missionName}"; } @@ -406,7 +412,7 @@ namespace Barotrauma else { GUITickBox tickBox = null; - if (!isMissionInNextLocation && mission.Prefab.ShowInMenus) + if (!isMissionInNextLocation && mission.Prefab.ShowInMenus && !mission.Prefab.IsSideObjective) { tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionNameBlock.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionNameBlock.Padding.X, 0) }, label: string.Empty) { @@ -539,7 +545,7 @@ namespace Barotrauma int missionCount = 0; if (GameMain.GameSession != null && Campaign.Map?.CurrentLocation?.SelectedMissions != null) { - missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m)); + missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m) && !m.Prefab.IsSideObjective); } return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}"); } @@ -551,9 +557,9 @@ namespace Barotrauma OnClicked = (GUIButton btn, object obj) => { if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && - missionList.Content.Children.Any(c => c.UserData is Mission { Prefab.ShowInMenus: true } mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation))) + missionList.Content.Children.Any(c => c.UserData is Mission { Prefab.ShowInMenus: true, Prefab.IsSideObjective: false } mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation))) { - var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), [TextManager.Get("yes"), TextManager.Get("no")]); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => { StartRound?.Invoke(); @@ -648,7 +654,7 @@ namespace Barotrauma private void UpdateMaxMissions(Location location) { - hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.TotalMaxMissionCount; + hasMaxMissions = Campaign.NumberOfSelectableMissionsAtLocation(location) >= Campaign.Settings.TotalMaxMissionCount; } public readonly struct PlayerBalanceElement diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index ecbb6fa55..18184ae38 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Diagnostics; using System.Linq; -using System.Transactions; namespace Barotrauma { @@ -349,14 +348,25 @@ namespace Barotrauma //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTargetBackground); - if (Level.Loaded == null) + if (Level.Loaded != null) { - graphics.Clear(new Color(11, 18, 26, 255)); + Level.Loaded.DrawBack(graphics, spriteBatch, cam); + } + else if (GameMain.GameSession.GameMode is TestGameMode testMode) + { + graphics.Clear(testMode.BackgroundParams.BackgroundColor); + + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap); + testMode.BackgroundParams.DrawBackgrounds(spriteBatch, cam); + spriteBatch.End(); + + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, cam.Transform); + testMode.BackgroundParams.DrawWaterParticles(spriteBatch, cam, testMode.WaterParticleOffset); + spriteBatch.End(); } else { - //graphics.Clear(new Color(255, 255, 255, 255)); - Level.Loaded.DrawBack(graphics, spriteBatch, cam); + graphics.Clear(new Color(11, 18, 26, 255)); } spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 724a5836f..e3d878e81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -382,6 +382,10 @@ namespace Barotrauma GameMain.LuaCs.CheckInitialize(); } } + else if (GameMain.Client.FileReceiver.ActiveTransfers.None()) + { + GameMain.Client.RequestFile(FileTransferType.Mod, currentDownload.Name, currentDownload.Hash.StringRepresentation); + } } public void CurrentDownloadFinished(FileReceiver.FileTransferIn transfer) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 629d8ea27..c2cc8606a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2221,11 +2221,7 @@ namespace Barotrauma if (GameMain.Client == null) { return true; } //the player presumably no longer wants to be afk if they clicked the start button - if (afkBox.Selected) - { - afkBox.Flash(GUIStyle.Green); - } - afkBox.Selected = false; + SetAFKSelected(false); if (CampaignSetupFrame.Visible && CampaignSetupUI != null) { @@ -2357,7 +2353,7 @@ namespace Barotrauma if (GameMain.Client != null) { - afkBox.Visible = !GameMain.Client.IsServerOwner && GameMain.Client.ServerSettings.AllowAFK; + afkBox.Visible = GameMain.Client.IsServerOwner || GameMain.Client.ServerSettings.AllowAFK; GameMain.Client.Voting.ResetVotes(GameMain.Client.ConnectedClients); joinOnGoingRoundButton.OnClicked = (btn, userdata) => { @@ -3082,6 +3078,15 @@ namespace Barotrauma } } + public void SetAFKSelected(bool selected) + { + if (afkBox.Selected != selected) + { + afkBox.Flash(GUIStyle.Green); + afkBox.Selected = selected; + } + } + public void SetAutoRestart(bool enabled, float timer = 0.0f) { autoRestartBox.Selected = enabled; @@ -4994,7 +4999,7 @@ namespace Barotrauma }; micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable"); - chatInput.Select(); + chatInput.Select(ignoreSelectSound: true); } //this needs to be done even if we're using the existing chatinput instance instead of creating a new one, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index db7d383dd..2861b56b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1126,6 +1126,10 @@ namespace Barotrauma { new GUIMessageBox(TextManager.Get("error"), TextManager.Get("CannotJoinSteamServer.SteamNotInitialized")); } + else if (endpoint is EosP2PEndpoint && !EosInterface.Core.IsInitialized) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("EosStatus.NotInitialized")); + } else { JoinServer(endpoint.ToEnumerable().ToImmutableArray(), ""); @@ -1183,9 +1187,29 @@ namespace Barotrauma endpointBox.OnTextChanged += (textBox, text) => { - okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); + okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); return true; }; + + // Connect on enter press, gotta go fast + endpointBox.OnEnterPressed += (textBox, text) => + { + if (okButton.Enabled) + { + if (okButton.PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(okButton.ClickSound); + } + okButton.OnClicked.Invoke(okButton, okButton.UserData); + } + return true; + }; + + // Focus on and select all the text, for easier input/deletion (after the dialog is shown, apparently it takes a moment) + CoroutineManager.Invoke(() => + { + endpointBox.Select(ignoreSelectSound: true); + }, 0.1f); } private void RemoveMsgFromServerList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 1e9ef8dd8..37b5d6e5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -40,6 +40,8 @@ namespace Barotrauma #region Transform Editor private const float TransformWidgetOffset = 300f; + private const float RotationSnapIncrement = MathF.PI / 36f; + private const float ScaleSnapIncrement = 0.1f; private GUITickBox rotateToolToggle, scaleToolToggle; public bool TransformWidgetSelected => TransformWidget.IsSelected; @@ -141,6 +143,7 @@ namespace Barotrauma if (rotateToolToggle.Selected) { transformCommand.RotationRad = MathUtils.VectorToAngle(PlayerInput.MousePosition - Cam.WorldToScreen(transformCommand.Pivot)); + if (!PlayerInput.IsShiftDown()) { transformCommand.RotationRad = MathUtils.RoundTowardsClosest(transformCommand.RotationRad.Value, RotationSnapIncrement); } rotationString = TextManager.GetWithVariable("SubEditor.TransformWidget.Rotation", "[value]", MathHelper.ToDegrees(transformCommand.RotationRad.Value).ToString("0.000", CultureInfo.CurrentCulture)); } @@ -148,7 +151,9 @@ namespace Barotrauma LocalizedString scaleString = null; if (scaleToolToggle.Selected) { - transformCommand.ScaleMult = Math.Clamp(Vector2.Distance(PlayerInput.MousePosition, Cam.WorldToScreen(transformCommand.Pivot)) / (TransformWidgetOffset * GUI.Scale), transformCommand.MinScale, transformCommand.MaxScale); + transformCommand.ScaleMult = Vector2.Distance(PlayerInput.MousePosition, Cam.WorldToScreen(transformCommand.Pivot)) / (TransformWidgetOffset * GUI.Scale); + if (!PlayerInput.IsShiftDown()) { transformCommand.ScaleMult = MathUtils.RoundTowardsClosest(transformCommand.ScaleMult.Value, ScaleSnapIncrement); } + transformCommand.ScaleMult = Math.Clamp(transformCommand.ScaleMult.Value, transformCommand.MinScale, transformCommand.MaxScale); scaleString = TextManager.GetWithVariable("SubEditor.TransformWidget.Scale", "[value]", transformCommand.ScaleMult.Value.ToString("0.000", CultureInfo.CurrentCulture)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index f8459f3bd..09e176792 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -260,8 +260,16 @@ namespace Barotrauma { continue; } - split[j] = split[j].Replace(" & ", " & "); - xmlContentByLanguage[languageName].Add($"<{split[0]}>{split[j]}"); + + string textContent = split[j]; + if (textContent == "#NAME?") + { + throw new Exception( + $"Error while converting csv to xml: #NAME? value found on line {row}, language: {languageName}." + + " This indicates a missing value in the csv file (some text got accidentally converted to a broken formula in the localization sheet?)."); + } + textContent = textContent.Replace(" & ", " & "); + xmlContentByLanguage[languageName].Add($"<{split[0]}>{textContent}"); } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 3393193b3..9a12c3b0c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.7.2 + 1.11.4.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index c324f28fb..ddb9b1754 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.7.2 + 1.11.4.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 7b9572498..b21ba47c4 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.7.2 + 1.11.4.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 9256ce179..9ac7207a5 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.7.2 + 1.11.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 08637b79a..fddacb624 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.7.2 + 1.11.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index f4a7ae58a..59c91a4a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -53,7 +53,7 @@ namespace Barotrauma { if (!Enabled) { return 1000.0f; } - Vector2 comparePosition = recipient.SpectatePos == null ? recipient.Character.WorldPosition : recipient.SpectatePos.Value; + Vector2 comparePosition = recipient.SpectatePos ?? recipient.Character.WorldPosition; float distance = Vector2.Distance(comparePosition, WorldPosition); if (recipient.Character?.ViewTarget != null) @@ -199,7 +199,9 @@ namespace Barotrauma UInt16 networkUpdateID = msg.ReadUInt16(); byte inputCount = msg.ReadByte(); - if (AllowInput) { Enabled = true; } + // Doesn't seem to work consistently (at least with simulated long loading time 120), because sometimes there's some stun on the character. Anyway, can't see why we'd have to check AllowInput here. + //if (AllowInput) { Enabled = true; } + Enabled = true; for (int i = 0; i < inputCount; i++) { @@ -802,9 +804,7 @@ namespace Barotrauma if (msg.LengthBytes - initialMsgLength >= 255 && restrictMessageSize) { - string errorMsg = $"Error when writing character spawn data for \"{Name}\": data exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchData", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.AddWarning($"Character spawn data for \"{Name}\" exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})"); } TryWriteStatus(msg); @@ -820,9 +820,7 @@ namespace Barotrauma msg.WriteBoolean(false); if (msgLengthBeforeStatus < 255) { - string errorMsg = $"Error when writing character spawn data for \"{Name}\": status data caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchDataForStatus", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError($"Character spawn data for \"{Name}\" caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})"); } } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6bb3c990f..7ee4200cd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -278,7 +278,14 @@ namespace Barotrauma characterInfo.ApplyDeathEffects(); } c.CharacterInfo = characterInfo; - SetClientCharacterData(c); + + // Only create new character data if the connected client has an active character (which they might not, + // eg. if they are in the lobby). Otherwise the CharacterCampaignData constructor would fall back to a new + // Character object, overwriting the inventory and wallet with empty values. + if (c.Character != null) + { + SetClientCharacterData(c); + } } //refresh the character data of clients who aren't in the server anymore @@ -1105,10 +1112,6 @@ namespace Barotrauma bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } } - else - { - GameServer.Log($"{sender.Name} attempted to buy or sell items without having access to a store NPC.", ServerLog.MessageType.Error); - } if ((purchasedUpgrades.Any() || purchasedItemSwaps.Any()) && HasCampaignInteractionAvailable(sender, InteractionType.Upgrade)) @@ -1142,10 +1145,6 @@ namespace Barotrauma } } } - else - { - GameServer.Log($"{sender.Name} attempted to buy upgrades without having access to an NPC offering upgrades.", ServerLog.MessageType.Error); - } } private bool HasCampaignInteractionAvailable(Client sender, InteractionType interactionType) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionSelectorComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionSelectorComponent.cs new file mode 100644 index 000000000..ff7c29aa4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionSelectorComponent.cs @@ -0,0 +1,48 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma.Items.Components +{ + partial class ConnectionSelectorComponent : ItemComponent + { + private CoroutineHandle sendStateCoroutine; + private int lastSentConnectionIndex; + private float sendStateTimer; + + partial void OnStateChanged() + { + sendStateTimer = 0.5f; + if (sendStateCoroutine == null) + { + sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay()); + } + } + + private IEnumerable SendStateAfterDelay() + { + while (sendStateTimer > 0.0f) + { + sendStateTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + + if (item.Removed || GameMain.NetworkMember == null) + { + yield return CoroutineStatus.Success; + } + + sendStateCoroutine = null; + if (lastSentConnectionIndex != selectedConnectionIndex) + { + item.CreateServerEvent(this); + } + yield return CoroutineStatus.Success; + } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + msg.WriteRangedInteger(selectedConnectionIndex, 0, 255); + lastSentConnectionIndex = selectedConnectionIndex; + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 8e345a566..d3aa1d893 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1857,6 +1857,9 @@ namespace Barotrauma.Networking } else { + //client presumably isn't afk if they clicked to start a round + sender.AFK = false; + bool continueCampaign = inc.ReadBoolean(); if (mpCampaign != null && mpCampaign.GameOver || continueCampaign) { @@ -2042,7 +2045,8 @@ namespace Barotrauma.Networking } } - if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) + //don't send the campaign save if there's any other transfers running (client waiting for subs, mods, or already transferring the campaign save) + if (FileSender.ActiveTransfers.None(t => t.Connection == c.Connection)) { FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.SavePath); c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now); @@ -3077,7 +3081,7 @@ namespace Barotrauma.Networking WayPoint jobItemSpawnPoint = mainSubWaypoints != null ? mainSubWaypoints[i] : spawnWaypoints[i]; Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer: true, hasAi: false); - spawnedCharacter.AnimController.Frozen = true; + //spawnedCharacter.AnimController.Frozen = true; spawnedCharacter.TeamID = teamID; teamClients[i].Character = spawnedCharacter; var characterData = campaign?.GetClientCharacterData(teamClients[i]); @@ -4359,7 +4363,7 @@ namespace Barotrauma.Networking public void SetClientCharacter(Client client, Character newCharacter) { - if (client == null) return; + if (client == null) { return; } //the client's previous character is no longer a remote player if (client.Character != null) @@ -4385,13 +4389,14 @@ namespace Barotrauma.Networking newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID; } - if (newCharacter.Info != null && newCharacter.Info.Character == null) + if (newCharacter.Info is { Character: null }) { newCharacter.Info.Character = newCharacter; } newCharacter.SetOwnerClient(client); newCharacter.Enabled = true; + newCharacter.AnimController.Frozen = false; client.Character = newCharacter; client.CharacterInfo = newCharacter.Info; CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 2cd089631..a3262596c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -128,11 +128,11 @@ namespace Barotrauma.Networking //remove old events that have been sent to all clients, they are redundant now // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced - if (GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) + if (GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration) { events.RemoveAll(e => (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && - e.CreateTime < Timing.TotalTime - NetConfig.EventRemovalTime); + e.CreateTime < Timing.TotalTime - server.ServerSettings.EventRemovalTime); } for (int i = events.Count - 1; i >= 0; i--) @@ -226,7 +226,7 @@ namespace Barotrauma.Networking if (Timing.TotalTime - lastWarningTime > 5.0 && Timing.TotalTime - lastSentToAnyoneTime > 10.0 && - GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) + GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; string warningMsg = $"WARNING: ServerEntityEventManager is lagging behind! Last sent id: {lastSentToAnyone}, latest create id: {ID}"; @@ -240,8 +240,8 @@ namespace Barotrauma.Networking ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1)); if (firstEventToResend != null && - GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration && - ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime)) + GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration && + ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > server.ServerSettings.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > server.ServerSettings.OldEventKickTime)) { // This event is 10 seconds older than the last one we've successfully sent, // kick everyone that hasn't received it yet, this is way too old diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 5f872965c..3e1a11c33 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -55,13 +55,13 @@ namespace Barotrauma.Networking PasswordRetries = 0; PasswordSalt = null; UpdateTime = Timing.TotalTime + Timing.Step * 3.0; - TimeOut = NetworkConnection.TimeoutThreshold; + TimeOut = NetworkConnection.TimeoutThresholdNotInGame; AuthSessionStarted = false; } public void Heartbeat() { - TimeOut = NetworkConnection.TimeoutThreshold; + TimeOut = NetworkConnection.TimeoutThresholdNotInGame; } } @@ -124,7 +124,7 @@ namespace Barotrauma.Networking protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) { - pendingClient.TimeOut = NetworkConnection.TimeoutThreshold; + pendingClient.TimeOut = NetworkConnection.TimeoutThresholdNotInGame; if (pendingClient.InitializationStep != initializationStep) { return; } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 8a689bb2f..eb683a2ad 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.7.2 + 1.11.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]MissionVariantsTest/Missions.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]MissionVariantsTest/Missions.xml new file mode 100644 index 000000000..bed2c1921 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]MissionVariantsTest/Missions.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]MissionVariantsTest/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]MissionVariantsTest/filelist.xml new file mode 100644 index 000000000..1ae2c16a2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]MissionVariantsTest/filelist.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index f7122bff5..e84df22f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1223,6 +1223,7 @@ namespace Barotrauma if (otherCharacter.SelectedCharacter == null || !otherCharacter.SelectedCharacter.IsDead || otherCharacter.SelectedCharacter.TeamID != Character.TeamID || + otherCharacter.IsPet || otherCharacter.IsInstigator) { continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 7b6ec2cc6..cf7299e32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -114,7 +114,7 @@ namespace Barotrauma if (target.Submarine != character.Submarine) { return; } Reset(); TargetCharacter = target; - targetBody = target.AnimController.Collider.FarseerBody; + targetBody = target.AnimController.MainLimb.body.FarseerBody; attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 092c07626..42e35c1b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -769,7 +769,7 @@ namespace Barotrauma Attack attack = GetAttackDefinition(weapon); if (attack != null) { - lethalDmg = attack.GetTotalDamage(); + lethalDmg = attack.GetTotalCharacterDamage(); float max = lethalDmg + 1; if (weapon.Item.HasTag(Tags.StunnerItem)) { @@ -795,7 +795,7 @@ namespace Barotrauma Attack attack = GetAttackDefinition(weapon); if (attack != null) { - lethalDmg = attack.GetTotalDamage(); + lethalDmg = attack.GetTotalCharacterDamage(); float stunDmg = ApproximateStunDamage(weapon, attack); float diff = stunDmg - lethalDmg; if (diff < 0) @@ -809,7 +809,7 @@ namespace Barotrauma { // Cannot do stun damage -> use the melee damage to determine the priority. Attack attack = GetAttackDefinition(weapon); - priority = attack?.GetTotalDamage() ?? priority / 2; + priority = attack?.GetTotalCharacterDamage() ?? priority / 2; } // Reduce the priority of the weapon, if we don't have requires skills to use it. float startPriority = priority; @@ -960,7 +960,7 @@ namespace Barotrauma Attack attack = GetAttackDefinition(weapon); if (attack != null) { - lethalDmg = attack.GetTotalDamage(); + lethalDmg = attack.GetTotalCharacterDamage(); } return lethalDmg; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs index 9fc63b0dd..2f0f8ec71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Linq; namespace Barotrauma @@ -94,6 +95,7 @@ namespace Barotrauma if (potentialDeconstructor?.InputContainer == null) { continue; } if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; } if (!potentialDeconstructor.Item.HasAccess(character)) { continue; } + if (Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) { continue; } float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f); if (distFactor > bestDistFactor) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs index 4f7b6d28b..781ed4746 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -30,6 +31,7 @@ namespace Barotrauma if (character.Submarine == null || Item.ItemList.None(it => it.GetComponent() != null && + !it.IgnoreByAI(character) && it.IsInteractable(character) && character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true))) { @@ -60,6 +62,9 @@ namespace Barotrauma protected override bool IsValidTarget(Item target) { if (target == null || target.Removed) { return false; } + //bots can't handle deconstructing items that require another item to deconstruct, let's not try to do that + //in the vanilla game, this means unidentified genetic materials, which we don't want to "deconstruct" anyway + if (target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) { return false; } // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 306fb786e..42cbd4de6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -98,18 +98,11 @@ namespace Barotrauma vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; if (affliction.Strength > affliction.Prefab.TreatmentThreshold) { - if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + //vitality loss is not required to treat this affliction -> evaluate the strength of the affliction too + if (!affliction.Prefab.VitalityLossRequiredForTreatment) { vitality -= affliction.Strength; } - else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) - { - vitality -= affliction.Strength; - } - else if (affliction.Prefab == AfflictionPrefab.HuskInfection) - { - vitality -= affliction.Strength; - } } } return Math.Clamp(vitality, 0, 100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 658c83afb..5b13a8e36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -437,8 +437,25 @@ namespace Barotrauma public bool TargetItemsMatchItem(Item item, Identifier option = default) { if (item == null) { return false; } - if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct && !Item.DeconstructItems.Contains(item)) { return true; } - if (Identifier == Tags.DontDeconstructThis && Item.DeconstructItems.Contains(item)) { return true; } + + if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct) + { + if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) && + //only allow deconstructing if there are deconstruction recipes that + item.Prefab.DeconstructItems.Any(deconstructItem => + //1. don't require any additional items (bots can't handle that) + deconstructItem.RequiredOtherItem.None() && + //2. don't require a research station (bots don't know how to use those) + (deconstructItem.RequiredDeconstructor.Length == 0 || deconstructItem.RequiredDeconstructor.Any(d => d != Tags.GeneticResearchStation)))) + { + return true; + } + } + else if (Identifier == Tags.DontDeconstructThis) + { + if (Item.DeconstructItems.Contains(item)) { return true; } + } + ImmutableArray targetItems = GetTargetItems(option); return TargetItemsMatchItem(targetItems, item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 6d49180c4..411aa8b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -216,6 +216,7 @@ namespace Barotrauma { UpdateTemporaryAnimations(); UpdateAnim(deltaTime); + CheckRopeState(); } protected abstract void UpdateAnim(float deltaTime); @@ -1134,6 +1135,25 @@ namespace Barotrauma character.TeleportTo(pos); } + protected void CheckRopeState() + { + if (!shouldHangWithRope) + { + StopHangingWithRope(); + } + if (!shouldHoldToRope) + { + StopHoldingToRope(); + } + if (!shouldBeDraggedWithRope) + { + StopGettingDraggedWithRope(); + } + shouldHoldToRope = false; + shouldHangWithRope = false; + shouldBeDraggedWithRope = false; + } + private void StartAnimation(Animation animation) { if (animation == Animation.UsingItem) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 5c5fde8a8..fb8754e55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -478,21 +478,6 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - if (!shouldHangWithRope) - { - StopHangingWithRope(); - } - if (!shouldHoldToRope) - { - StopHoldingToRope(); - } - if (!shouldBeDraggedWithRope) - { - StopGettingDraggedWithRope(); - } - shouldHoldToRope = false; - shouldHangWithRope = false; - shouldBeDraggedWithRope = false; } void UpdateStanding() @@ -1256,34 +1241,25 @@ namespace Barotrauma float prevVitality = target.Vitality; bool wasCritical = prevVitality < 0.0f; - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code + float cprBoost = character.GetStatValue(StatTypes.CPRBoost); + float skill = character.GetSkillLevel(Tags.MedicalSkill); + bool oxygenAvailable = target.OxygenAvailable >= CharacterHealth.InsufficientOxygenThreshold; + + //Serverside code + if (oxygenAvailable && GameMain.NetworkMember is not { IsClient: true }) { target.Oxygen += deltaTime * 0.5f; //Stabilize them - } - - float cprBoost = character.GetStatValue(StatTypes.CPRBoost); - - int skill = (int)character.GetSkillLevel(Tags.MedicalSkill); - - if (GameMain.NetworkMember is not { IsClient: true }) - { if (cprBoost >= 1f) { //prevent the patient from suffocating no matter how fast their oxygen level is dropping target.Oxygen = Math.Max(target.Oxygen, -10.0f); } - } - - //Serverside code - if (GameMain.NetworkMember is not { IsClient: true }) - { if (target.Oxygen < -10.0f) { //stabilize the oxygen level but don't allow it to go positive and revive the character yet float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); - character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required - if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we + target.Oxygen += stabilizationAmount * deltaTime; } } @@ -1317,7 +1293,7 @@ namespace Barotrauma } //need to CPR for at least a couple of seconds before the target can be revived //(reviving the target when the CPR has barely started looks strange) - if (cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true }) + if (oxygenAvailable && cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true }) { float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); @@ -1343,7 +1319,7 @@ namespace Barotrauma //got the character back into a non-critical state, increase medical skill //BUT only if it has been more than 10 seconds since the character revived someone //otherwise it's easy to abuse the system by repeatedly reviving in a low-oxygen room - if (!target.IsDead) + if (!target.IsDead || !oxygenAvailable) { target.CharacterHealth.RecalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 9c899d04b..5b7d36724 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -76,7 +76,7 @@ namespace Barotrauma get { return frozen; } set { - if (frozen == value) return; + if (frozen == value) { return; } frozen = value; @@ -1063,7 +1063,8 @@ namespace Barotrauma } } - public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true) + /// Should the character be immediately considered "in water" if it's outside hulls (normally checked in Update) + public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true, bool setInWater = false) { Vector2 findPos = worldPosition == null ? this.WorldPosition : (Vector2)worldPosition; if (!MathUtils.IsValid(findPos)) @@ -1076,6 +1077,10 @@ namespace Barotrauma } Hull newHull = Hull.FindHull(findPos, currentHull); + if (setInWater && newHull == null) + { + inWater = true; + } if (newHull == currentHull) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 899984f8c..bfed4ae7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -403,14 +403,19 @@ namespace Barotrauma return (Duration == 0.0f) ? dmg : dmg * deltaTime; } - public float GetTotalDamage(bool includeStructureDamage = false) + /// + /// Returns the total damage (vitality decrease) this attack causes on characters. + /// + public float GetTotalCharacterDamage() { - float totalDamage = includeStructureDamage ? StructureDamage : 0.0f; + float totalDamage = 0.0f; foreach (Affliction affliction in Afflictions.Keys) { - totalDamage += affliction.GetVitalityDecrease(null); + float afflictionVitalityDecrease = affliction.GetVitalityDecrease(null); + if (affliction.AffectedByAttackMultipliers) { afflictionVitalityDecrease *= DamageMultiplier; } + totalDamage += afflictionVitalityDecrease; } - return totalDamage * DamageMultiplier; + return totalDamage; } public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 0d57b3577..7213bb625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -30,7 +30,7 @@ namespace Barotrauma partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync { - public readonly static List CharacterList = new List(); + public static readonly List CharacterList = new List(); public static int CharacterUpdateInterval = 1; private static int characterUpdateTick = 1; @@ -42,7 +42,13 @@ namespace Barotrauma partial void UpdateLimbLightSource(Limb limb); - private bool enabled = true; + private bool initialized; + private bool enabled; + //characters start disabled in the multiplayer mode, and are enabled if/when + // - controlled by the player + // - client receives a position update from the server + // - server receives an input message from the client controlling the character + // - if an AICharacter, the server enables it when close enough to any of the players public bool Enabled { get @@ -51,7 +57,12 @@ namespace Barotrauma } set { - if (value == enabled) { return; } + if (initialized && value == enabled) + { + // Ensure that we'll set the value and run the code below at least once, because otherwise the states might be out of sync. + return; + } + initialized = true; if (Removed) { @@ -83,7 +94,6 @@ namespace Barotrauma //we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs item.body.Enabled = true; } - } AnimController.Collider.Enabled = value; } @@ -112,6 +122,13 @@ namespace Barotrauma if (!CharacterList.Contains(this)) { CharacterList.Add(this); } if (AiTarget != null && !AITarget.List.Contains(AiTarget)) { AITarget.List.Add(AiTarget); } } + if (Inventory != null) + { + foreach (var item in Inventory.FindAllItems(recursive: true)) + { + item.IsActive = !disabledByEvent; + } + } } } @@ -1625,18 +1642,14 @@ namespace Barotrauma PressureProtection = int.MaxValue; } - AnimController.SetPosition(ConvertUnits.ToSimUnits(position)); + CharacterHealth.CheckForErrors(); - AnimController.FindHull(null); + AnimController.SetPosition(ConvertUnits.ToSimUnits(position)); + AnimController.FindHull(setInWater: true); if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; } CharacterList.Add(this); - - //characters start disabled in the multiplayer mode, and are enabled if/when - // - controlled by the player - // - client receives a position update from the server - // - server receives an input message from the client controlling the character - // - if an AICharacter, the server enables it when close enough to any of the players + Enabled = GameMain.NetworkMember == null; if (info != null) @@ -3311,17 +3324,22 @@ namespace Barotrauma public static void UpdateAll(float deltaTime, Camera cam) { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) // single player or server { foreach (Character c in CharacterList) { - if (c is not AICharacter && !c.IsRemotePlayer) { continue; } - - if (c.IsPlayer || (c.IsBot && !c.IsDead)) + // TODO: The logic below seems to be overly complicated and quite confusing + if (c is not AICharacter && !c.IsRemotePlayer) { continue; } // confusing -> what this line is intended for? local player? But that's handled below... + if (c.IsRemotePlayer) + { + // Let the client tell when to enable the character. If we force it enabled here, it may e.g. get killed while still loading a round. + continue; + } + if (c.IsLocalPlayer || (c.IsBot && !c.IsDead)) { c.Enabled = true; } - else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) // mp server { //disable AI characters that are far away from all clients and the host's character and not controlled by anyone float closestPlayerDist = c.GetDistanceToClosestPlayer(); @@ -3338,7 +3356,7 @@ namespace Barotrauma c.Enabled = true; } } - else if (Submarine.MainSub != null) + else if (Submarine.MainSub != null) // sp only? { //disable AI characters that are far away from the sub and the controlled character float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition); @@ -3382,7 +3400,7 @@ namespace Barotrauma foreach (Character character in GameMain.LuaCs.Game.UpdatePriorityCharacters) { if (character.Removed) { continue; } - + Debug.Assert(character is { Removed: false }); character.Update(deltaTime, cam); } @@ -3444,8 +3462,7 @@ namespace Barotrauma foreach (Item item in Inventory.GetAllItems(checkForDuplicates: false)) { if (item.body == null || item.body.Enabled) { continue; } - item.SetTransform(SimPosition, 0.0f); - item.Submarine = Submarine; + item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine); } } @@ -4600,7 +4617,10 @@ namespace Barotrauma SetStun(stun); - if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) + if (attacker != null && attacker != this && + attacker.IsOnPlayerTeam && + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { if (attacker.TeamID == TeamID) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 2a8102078..2dbe048f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -85,6 +85,8 @@ namespace Barotrauma public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime; + public bool AffectedByAttackMultipliers => Prefab.AffectedByAttackMultipliers; + public float Duration; /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 541814c6a..31abf6789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -164,15 +164,18 @@ namespace Barotrauma } break; case InfectionState.Transition: - if (character == Character.Controlled) + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { + if (character == Character.Controlled) + { #if CLIENT - GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); + GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); #endif - } - else if (character.IsBot) - { - character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier()); + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier()); + } } break; case InfectionState.Active: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 132b79833..cb36b1196 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -699,7 +699,13 @@ namespace Barotrauma /// and the health UI will render the affected limb in green rather than red. /// public readonly bool IsBuff; - + + /// + /// Should the affliction be affected by damage multipliers on an attack (e.g. when the attacker has talents that boost damage). + /// By default, afflictions defined as buffs aren't affected. + /// + public readonly bool AffectedByAttackMultipliers; + /// /// If set to true, this affliction can affect characters that are marked as /// machines, such as the Fractal Guardian. @@ -780,6 +786,14 @@ namespace Barotrauma /// public readonly float TreatmentSuggestionThreshold; + /// + /// Does the affliction need to have caused some amount of vitality loss for bots to consider treating it? + /// Normally bots use vitality loss as a way to determine what kind of injuries need treatment, but some afflictions (e.g. poisons, infections) + /// might require treatment regardless of the vitality loss. If disabled, the bots will use the strength of the affliction to evaluate the severity instead of the vitality loss. + /// Defaults to true for all afflictions that aren't of the type Paralysis, Poison or HuskInfection. + /// + public readonly bool VitalityLossRequiredForTreatment; + /// /// Bots will not try to treat the affliction if the character has any of these afflictions /// @@ -838,14 +852,15 @@ namespace Barotrauma public readonly bool DamageParticles; /// - /// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target. - /// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile. - /// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly. + /// A modifier that affects how much medical skill is increased when you apply this affliction on a target. + /// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile, and the modifier is multiplied by the amount of vitality the enemy lost. + /// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly, and the modifier is multiplied by the strength of the affliction the target gained. /// public readonly float MedicalSkillGain; /// - /// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target. + /// A modifier that affects how much weapons skill is increased when you apply the affliction on a target. + /// Multiplied by the amount of vitality the enemy lost. /// The skill is increased only when the target is hostile. /// public readonly float WeaponsSkillGain; @@ -925,6 +940,7 @@ namespace Barotrauma ShowDescriptionInTooltip = element.GetAttributeBool(nameof(ShowDescriptionInTooltip), true); IsBuff = element.GetAttributeBool(nameof(IsBuff), false); + AffectedByAttackMultipliers = element.GetAttributeBool(nameof(AffectedByAttackMultipliers), def: !IsBuff); AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true); ShowBarInHealthMenu = element.GetAttributeBool("showbarinhealthmenu", true); @@ -977,6 +993,11 @@ namespace Barotrauma TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f)); TreatmentSuggestionThreshold = element.GetAttributeFloat(nameof(TreatmentSuggestionThreshold), TreatmentThreshold); + bool alwaysRequiresTreatment = AfflictionType == ParalysisType || AfflictionType == PoisonType || this is AfflictionPrefabHusk; + + VitalityLossRequiredForTreatment = element.GetAttributeBool(nameof(VitalityLossRequiredForTreatment), + def: !alwaysRequiresTreatment); + DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f); BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 8c9362841..110d8eca2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -309,6 +309,19 @@ namespace Barotrauma InitProjSpecific(element, character); } + public void CheckForErrors() + { + for (int i = 0; i < limbHealths.Count; i++) + { + if (Character.AnimController.Limbs.None(l => l.HealthIndex == i)) + { + DebugConsole.AddWarning( + $"Potential error in character {Character.DisplayName}: none of the limbs have been set to use the LimbHealth #{i}, and it will do nothing. " + + "Did you forget to set the HealthIndex values of the limbs?", contentPackage: Character.ContentPackage); + } + } + } + private void InitIrremovableAfflictions() { irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f)); @@ -1132,20 +1145,38 @@ namespace Barotrauma // We need to use another list of the afflictions when we call the status effects triggered by afflictions, // because those status effects may add or remove other afflictions while iterating the collection. - private readonly List afflictionsCopy = new List(); + private readonly List afflictionsCopy = []; + + private bool isApplyingAfflictionStatusEffects; public void ApplyAfflictionStatusEffects(ActionType type) { - afflictionsCopy.Clear(); - afflictionsCopy.AddRange(afflictions.Keys); - foreach (Affliction affliction in afflictionsCopy) + if (isApplyingAfflictionStatusEffects) { - affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction)); + //pretty hacky: if we're already in the process of applying afflictions' status effects + //(i.e. calling this method caused some additional afflictions to appear and trigger status effects) + //let's instantiate a new list so we don't end up modifying afflictionsCopy while enumerating it + foreach (Affliction affliction in afflictions.Keys.ToList()) + { + affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction)); + } + } + else + { + isApplyingAfflictionStatusEffects = true; + afflictionsCopy.Clear(); + afflictionsCopy.AddRange(afflictions.Keys); + isApplyingAfflictionStatusEffects = true; + foreach (Affliction affliction in afflictionsCopy) + { + affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction)); + } + isApplyingAfflictionStatusEffects = false; } } public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath() { - List currentAfflictions = GetAllAfflictions(true); + IEnumerable currentAfflictions = GetAllAfflictions(true); Affliction strongestAffliction = null; float largestStrength = 0.0f; @@ -1168,7 +1199,7 @@ namespace Barotrauma } private readonly List allAfflictions = new List(); - private List GetAllAfflictions(bool mergeSameAfflictions, Func predicate = null) + private IEnumerable GetAllAfflictions(bool mergeSameAfflictions, Func predicate = null) { allAfflictions.Clear(); if (!mergeSameAfflictions) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 963ea9a7c..d9226ce75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -843,7 +843,7 @@ namespace Barotrauma } } if (!foundMatchingModifier && random > affliction.Probability) { continue; } - float finalDamageModifier = damageMultiplier; + float finalDamageModifier = affliction.AffectedByAttackMultipliers ? damageMultiplier : 1.0f; if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { finalDamageModifier *= character.EmpVulnerability; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 10832764f..3b28cd186 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -14,14 +14,41 @@ namespace Barotrauma public enum SpawnLocationType { Any, + /// + /// Spawnpoint inside the main submarine. + /// MainSub, + /// + /// Spawnpoint inside an outpost. + /// Outpost, + /// + /// Spawnpoint on the main path through the level. + /// MainPath, + /// + /// Spawnpoint in a cave. Only valid if there are caves in the level. + /// Cave, + /// + /// Spawnpoint in an abyss cave. Only valid if there are abyss caves in the level. + /// AbyssCave, + /// + /// Spawnpoint in a ruin. Only valid if there are ruins in the level. + /// Ruin, + /// + /// Spawnpoint in a wreck. Only valid if there are wrecks in the level. + /// Wreck, + /// + /// Spawnpoint in a beacon station. Only valid if there are beacon stations in the level. + /// BeaconStation, + /// + /// A spawnpoint on the main path through the level. The difference to the type is that the closest possible spawnpoint is chosen. + /// NearMainSub } @@ -425,7 +452,7 @@ namespace Barotrauma } else { - spawnPointsWithCorrectType = potentialSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path); + spawnPointsWithCorrectType = potentialSpawnPoints; } if (spawnPointsWithCorrectType.Any()) { @@ -485,7 +512,12 @@ namespace Barotrauma } //spawnpoints that match the desired criteria found, choose the best one next - IEnumerable validSpawnPoints = potentialSpawnPoints; + // preferring non-path spawnpoints if there's any available + var nonPathSpawnPoints = potentialSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path); + var validSpawnPoints = + nonPathSpawnPoints.Any() && spawnPointType != SpawnType.Path ? + nonPathSpawnPoints : + potentialSpawnPoints; //don't spawn in an airlock module if there are other options var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 25829bd93..cd32310eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -8,9 +8,9 @@ using System.Xml.Linq; namespace Barotrauma { - partial class MissionPrefab : PrefabWithUintIdentifier + internal sealed partial class MissionPrefab : PrefabWithUintIdentifier, IImplementsVariants { - public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public static readonly PrefabCollection Prefabs = []; /// /// The keys here are for backwards compatibility, tying the old mission types to the appropriate class. @@ -42,7 +42,7 @@ namespace Barotrauma { "Combat".ToIdentifier(), typeof(CombatMission) } }; - public static readonly HashSet HiddenMissionTypes = new HashSet() { "GoTo".ToIdentifier(), "End".ToIdentifier() }; + public static readonly HashSet HiddenMissionTypes = ["GoTo".ToIdentifier(), "End".ToIdentifier()]; public class ReputationReward { @@ -58,110 +58,117 @@ namespace Barotrauma } } - private readonly ConstructorInfo constructor; + private ConstructorInfo constructor; - public readonly Identifier Type; + public Identifier Type { get; private set; } - public readonly Type MissionClass; + public Type MissionClass { get; private set; } - public readonly bool MultiplayerOnly, SingleplayerOnly; + public bool MultiplayerOnly { get; private set; } + public bool SingleplayerOnly { get; private set; } - public readonly Identifier TextIdentifier; + public Identifier TextIdentifier { get; private set; } - public readonly ImmutableHashSet Tags; + public ImmutableHashSet Tags { get; private set; } - public readonly LocalizedString Name; - public readonly LocalizedString Description; - public readonly LocalizedString SuccessMessage; - public readonly LocalizedString FailureMessage; - public readonly LocalizedString SonarLabel; - public readonly Identifier SonarIconIdentifier; + public LocalizedString Name { get; private set; } + public LocalizedString Description { get; private set; } + public LocalizedString SuccessMessage { get; private set; } + public LocalizedString FailureMessage { get; private set; } + public LocalizedString SonarLabel { get; private set; } + public Identifier SonarIconIdentifier { get; private set; } - public readonly Identifier AchievementIdentifier; + public Identifier AchievementIdentifier { get; private set; } - public readonly ImmutableList ReputationRewards; + public ImmutableList ReputationRewards { get; private set; } - public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> - DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); + public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> DataRewards = []; - public readonly int Commonness; + public int Commonness { get; private set; } /// /// Displayed difficulty (indicator) /// - public readonly int? Difficulty; + public int? Difficulty { get; private set; } public const int MinDifficulty = 1, MaxDifficulty = 4; /// /// The actual minimum difficulty of the level allowed for this mission to trigger. /// - public readonly int MinLevelDifficulty = 0; + public int MinLevelDifficulty { get; private set; } = 0; /// /// The actual maximum difficulty of the level allowed for this mission to trigger. /// - public readonly int MaxLevelDifficulty = 100; + public int MaxLevelDifficulty { get; private set; } = 100; - public readonly int Reward; + public int Reward { get; private set; } - public readonly float ExperienceMultiplier; + public float ExperienceMultiplier { get; private set; } // The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters. - public readonly ImmutableArray Headers; - public readonly ImmutableArray Messages; + public ImmutableArray Headers { get; private set; } + public ImmutableArray Messages { get; private set; } - public readonly bool AllowRetry; + public bool AllowRetry { get; private set; } - public readonly bool ShowSonarLabels; + public bool ShowSonarLabels { get; private set; } - public readonly bool ShowInMenus, ShowStartMessage; + public bool ShowInMenus { get; private set; } + public bool ShowStartMessage { get; private set; } - public readonly bool IsSideObjective; + /// + /// Makes the mission not count for the maximum mission limit, and forces it to always be selected when it's available in a level. + /// + public bool IsSideObjective { get; private set; } - public readonly bool AllowOtherMissionsInLevel; + public bool AllowOtherMissionsInLevel { get; private set; } - public readonly bool RequireWreck, RequireRuin, RequireBeaconStation, RequireThalamusWreck; - public readonly bool SpawnBeaconStationInMiddle; + public bool RequireWreck { get; private set; } + public bool RequireRuin { get; private set; } + public bool RequireBeaconStation { get; private set; } + public bool RequireThalamusWreck { get; private set; } + public bool SpawnBeaconStationInMiddle { get; private set; } - public readonly bool AllowOutpostNPCs; + public bool AllowOutpostNPCs { get; private set; } - public readonly Identifier ForceOutpostGenerationParameters; + public Identifier ForceOutpostGenerationParameters { get; private set; } - public readonly RespawnMode? ForceRespawnMode; + public RespawnMode? ForceRespawnMode { get; private set; } /// /// If set, the players can choose which outpost is used for the mission (selected from the outposts that have this tag). Only works in multiplayer. /// - public readonly Identifier AllowOutpostSelectionFromTag; + public Identifier AllowOutpostSelectionFromTag { get; private set; } - public readonly bool LoadSubmarines = true; + public bool LoadSubmarines { get; private set; } = true; /// /// If enabled, locations this mission takes place in cannot change their type /// - public readonly bool BlockLocationTypeChanges; + public bool BlockLocationTypeChanges { get; private set; } - public readonly bool ShowProgressBar; - public readonly bool ShowProgressInNumbers; - public readonly int MaxProgressState; - public readonly LocalizedString ProgressBarLabel; + public bool ShowProgressBar { get; private set; } + public bool ShowProgressInNumbers { get; private set; } + public int MaxProgressState { get; private set; } + public LocalizedString ProgressBarLabel { get; private set; } /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// - public readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes; + public List<(Identifier from, Identifier to)> AllowedConnectionTypes { get; private set; } /// /// The mission can only be received in these location types /// - public readonly List AllowedLocationTypes = new List(); + public readonly List AllowedLocationTypes = []; /// /// The mission can only happen in locations owned by this faction. In the mission mode, the location is forced to be owned by this faction. /// - public readonly Identifier RequiredLocationFaction; + public Identifier RequiredLocationFaction { get; private set; } /// /// Show entities belonging to these sub categories when the mission starts /// - public readonly List UnhideEntitySubCategories = new List(); + public List UnhideEntitySubCategories { get; private set; } public class TriggerEvent { @@ -186,22 +193,39 @@ namespace Barotrauma } } - public readonly List TriggerEvents = new List(); + public readonly List TriggerEvents = []; public LocationTypeChange LocationTypeChangeOnCompleted; - public readonly ContentXElement ConfigElement; + private readonly ContentXElement originalElement; + public ContentXElement ConfigElement { get; private set; } + + public Identifier VariantOf { get; } + public MissionPrefab ParentPrefab { get; set; } public MissionPrefab(ContentXElement element, MissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - ConfigElement = element; + ConfigElement = originalElement = element; - TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); + VariantOf = element.VariantOf(); + if (!VariantOf.IsEmpty) { return; } // Don't read the XML until the PrefabCollection loads the parent. + ParseConfigElement(); + } - Tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); + public void InheritFrom(MissionPrefab parent) + { + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement); + ParseConfigElement(parent); + } - Name = GetText(element.GetAttributeString("name", ""), "MissionName"); - Description = GetText(element.GetAttributeString("description", ""), "MissionDescription"); + private void ParseConfigElement(MissionPrefab variantOf = null) + { + TextIdentifier = ConfigElement.GetAttributeIdentifier("textidentifier", Identifier); + + Tags = [.. ConfigElement.GetAttributeIdentifierArray("tags", [])]; + + Name = GetText(ConfigElement.GetAttributeString("name", ""), "MissionName"); + Description = GetText(ConfigElement.GetAttributeString("description", ""), "MissionDescription"); LocalizedString GetText(string textTag, string textTagPrefix) { @@ -211,105 +235,100 @@ namespace Barotrauma } else { - return - //prefer finding a text based on the specific text tag defined in the mission config - TextManager.Get(textTag) - //2nd option: the "default" format (MissionName.SomeMission) - .Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}")) - //last option: use the text in the xml as-is with no localization - .Fallback(textTag); + return TextManager.Get(textTag) // Prefer finding a text based on the specific text tag defined in the mission config. + .Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}")) // 2nd option: the "default" format (MissionName.SomeMission). + .Fallback(textTag); // Last option: Use the text in the xml as-is with no localization. } } - Reward = element.GetAttributeInt(nameof(Reward), 1); - ExperienceMultiplier = element.GetAttributeFloat(nameof(ExperienceMultiplier), 1.0f); - AllowRetry = element.GetAttributeBool(nameof(AllowRetry), false); - ShowSonarLabels = element.GetAttributeBool(nameof(ShowSonarLabels), true); - ShowInMenus = element.GetAttributeBool(nameof(ShowInMenus), true); - ShowStartMessage = element.GetAttributeBool(nameof(ShowStartMessage), true); - IsSideObjective = element.GetAttributeBool("sideobjective", false); + Reward = ConfigElement.GetAttributeInt(nameof(Reward), 1); + ExperienceMultiplier = ConfigElement.GetAttributeFloat(nameof(ExperienceMultiplier), 1f); + AllowRetry = ConfigElement.GetAttributeBool(nameof(AllowRetry), false); + ShowSonarLabels = ConfigElement.GetAttributeBool(nameof(ShowSonarLabels), true); + ShowInMenus = ConfigElement.GetAttributeBool(nameof(ShowInMenus), true); + ShowStartMessage = ConfigElement.GetAttributeBool(nameof(ShowStartMessage), true); + IsSideObjective = ConfigElement.GetAttributeBool("sideobjective", false); - RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false); - RequireThalamusWreck = element.GetAttributeBool(nameof(RequireThalamusWreck), false); - RequireRuin = element.GetAttributeBool(nameof(RequireRuin), false); - RequireBeaconStation = element.GetAttributeBool(nameof(RequireBeaconStation), false); - SpawnBeaconStationInMiddle = element.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false); - if (RequireThalamusWreck) { RequireWreck = true; } + RequireWreck = ConfigElement.GetAttributeBool(nameof(RequireWreck), false); + RequireThalamusWreck = ConfigElement.GetAttributeBool(nameof(RequireThalamusWreck), false); + RequireRuin = ConfigElement.GetAttributeBool(nameof(RequireRuin), false); + RequireBeaconStation = ConfigElement.GetAttributeBool(nameof(RequireBeaconStation), false); + SpawnBeaconStationInMiddle = ConfigElement.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false); + RequireWreck |= RequireThalamusWreck; - LoadSubmarines = element.GetAttributeBool(nameof(LoadSubmarines), true); + LoadSubmarines = ConfigElement.GetAttributeBool(nameof(LoadSubmarines), true); - BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); - RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty); - Commonness = element.GetAttributeInt(nameof(Commonness), 1); - AllowOtherMissionsInLevel = element.GetAttributeBool(nameof(AllowOtherMissionsInLevel), true); + BlockLocationTypeChanges = ConfigElement.GetAttributeBool(nameof(BlockLocationTypeChanges), false); + RequiredLocationFaction = ConfigElement.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty); + Commonness = ConfigElement.GetAttributeInt(nameof(Commonness), 1); + AllowOtherMissionsInLevel = ConfigElement.GetAttributeBool(nameof(AllowOtherMissionsInLevel), true); - if (element.GetAttribute("difficulty") != null) + if (ConfigElement.GetAttribute("difficulty") != null) { - int difficulty = element.GetAttributeInt(nameof(Difficulty), MinDifficulty); + int difficulty = ConfigElement.GetAttributeInt(nameof(Difficulty), MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } - MinLevelDifficulty = element.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty); - MaxLevelDifficulty = element.GetAttributeInt(nameof(MaxLevelDifficulty), MaxLevelDifficulty); + MinLevelDifficulty = ConfigElement.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty); + MaxLevelDifficulty = ConfigElement.GetAttributeInt(nameof(MaxLevelDifficulty), MaxLevelDifficulty); MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100)); MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100); - AllowOutpostNPCs = element.GetAttributeBool(nameof(AllowOutpostNPCs), true); - ForceOutpostGenerationParameters = element.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty); - AllowOutpostSelectionFromTag = element.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty); + AllowOutpostNPCs = ConfigElement.GetAttributeBool(nameof(AllowOutpostNPCs), true); + ForceOutpostGenerationParameters = ConfigElement.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty); + AllowOutpostSelectionFromTag = ConfigElement.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty); - if (element.GetAttribute(nameof(ForceRespawnMode)) != null) + if (ConfigElement.GetAttribute(nameof(ForceRespawnMode)) != null) { - ForceRespawnMode = element.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound); + ForceRespawnMode = ConfigElement.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound); } - ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); - ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); - MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1); - string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), ""); + ShowProgressBar = ConfigElement.GetAttributeBool(nameof(ShowProgressBar), false); + ShowProgressInNumbers = ConfigElement.GetAttributeBool(nameof(ShowProgressInNumbers), false); + MaxProgressState = ConfigElement.GetAttributeInt(nameof(MaxProgressState), 1); + string progressBarLabel = ConfigElement.GetAttributeString(nameof(ProgressBarLabel), ""); ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel); - string successMessageTag = element.GetAttributeString("successmessage", ""); + string successMessageTag = ConfigElement.GetAttributeString("successmessage", ""); SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}"); if (!string.IsNullOrEmpty(successMessageTag)) { SuccessMessage = SuccessMessage - .Fallback(TextManager.Get(successMessageTag)) - .Fallback(successMessageTag); + .Fallback(TextManager.Get(successMessageTag)) + .Fallback(successMessageTag); } SuccessMessage = SuccessMessage.Fallback(TextManager.Get("missioncompleted")); - string failureMessageTag = element.GetAttributeString("failuremessage", ""); + string failureMessageTag = ConfigElement.GetAttributeString("failuremessage", ""); FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}"); if (!string.IsNullOrEmpty(failureMessageTag)) { FailureMessage = FailureMessage - .Fallback(TextManager.Get(failureMessageTag)) - .Fallback(failureMessageTag); + .Fallback(TextManager.Get(failureMessageTag)) + .Fallback(failureMessageTag); } FailureMessage = FailureMessage.Fallback(TextManager.Get("missionfailed")); - string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); - SonarLabel = - TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") - .Fallback(TextManager.Get(sonarLabelTag)) - .Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}")); + string sonarLabelTag = ConfigElement.GetAttributeString("sonarlabel", ""); + SonarLabel = TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") + .Fallback(TextManager.Get(sonarLabelTag)) + .Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}")); if (!string.IsNullOrEmpty(sonarLabelTag)) { SonarLabel = SonarLabel.Fallback(sonarLabelTag); } - SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", ""); + SonarIconIdentifier = ConfigElement.GetAttributeIdentifier("sonaricon", ""); - MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); - SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false); + MultiplayerOnly = ConfigElement.GetAttributeBool("multiplayeronly", false); + SingleplayerOnly = ConfigElement.GetAttributeBool("singleplayeronly", false); - AchievementIdentifier = element.GetAttributeIdentifier("achievementidentifier", ""); + AchievementIdentifier = ConfigElement.GetAttributeIdentifier("achievementidentifier", ""); - UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", Array.Empty()).ToList(); + UnhideEntitySubCategories = [.. ConfigElement.GetAttributeStringArray("unhideentitysubcategories", [])]; - var headers = new List(); - var messages = new List(); - AllowedConnectionTypes = new List<(Identifier from, Identifier to)>(); + List headers = []; + List messages = []; + AllowedConnectionTypes = []; for (int i = 0; i < 100; i++) { @@ -322,26 +341,24 @@ namespace Barotrauma } } - List reputationRewards = new List(); + List reputationRewards = []; int messageIndex = 0; - foreach (var subElement in element.Elements()) + foreach (ContentXElement subElement in ConfigElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "message": - if (messageIndex > headers.Count - 1) + if (messageIndex >= headers.Count) { headers.Add(string.Empty); messages.Add(string.Empty); } - headers[messageIndex] = - TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}") - .Fallback(TextManager.Get(subElement.GetAttributeString("header", ""))) - .Fallback(subElement.GetAttributeString("header", "")); - messages[messageIndex] = - TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}") - .Fallback(TextManager.Get(subElement.GetAttributeString("text", ""))) - .Fallback(subElement.GetAttributeString("text", "")); + headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}") + .Fallback(TextManager.Get(subElement.GetAttributeString("header", ""))) + .Fallback(subElement.GetAttributeString("header", "")); + messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}") + .Fallback(TextManager.Get(subElement.GetAttributeString("text", ""))) + .Fallback(subElement.GetAttributeString("text", "")); messageIndex++; break; case "locationtype": @@ -352,9 +369,7 @@ namespace Barotrauma } else { - AllowedConnectionTypes.Add(( - subElement.GetAttributeIdentifier("from", ""), - subElement.GetAttributeIdentifier("to", ""))); + AllowedConnectionTypes.Add((subElement.GetAttributeIdentifier("from", ""), subElement.GetAttributeIdentifier("to", ""))); } break; case "locationtypechange": @@ -375,7 +390,7 @@ namespace Barotrauma string operatingString = subElement.GetAttributeString("operation", string.Empty); if (!string.IsNullOrWhiteSpace(operatingString)) { - operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString); + operation = (SetDataAction.OperationType)Enum.Parse(typeof(SetDataAction.OperationType), operatingString); } DataRewards.Add((identifier, value, operation)); @@ -386,13 +401,13 @@ namespace Barotrauma break; } } - Headers = headers.ToImmutableArray(); - Messages = messages.ToImmutableArray(); - ReputationRewards = reputationRewards.ToImmutableList(); + Headers = [.. headers]; + Messages = [.. messages]; + ReputationRewards = [.. reputationRewards]; + + MissionClass = FindMissionClass(ConfigElement); + Type = ConfigElement.GetAttributeIdentifier(nameof(Type), Identifier.Empty); - MissionClass = FindMissionClass(element); - Type = element.GetAttributeIdentifier(nameof(Type), Identifier.Empty); - #if DEBUG if (MissionClass == typeof(MonsterMission) && SonarLabel.IsNullOrEmpty()) { @@ -403,17 +418,19 @@ namespace Barotrauma if (!LoadSubmarines && MissionClass != typeof(CombatMission)) { DebugConsole.AddWarning($"Potential error in mission {Identifier}: Disabling submarines is only intended for combat missions taking place in an outpost, and may lead to issues in other types of missions.", - contentPackage: element.ContentPackage); + contentPackage: ConfigElement.ContentPackage); } - constructor = FindMissionConstructor(element, MissionClass); + constructor = FindMissionConstructor(ConfigElement, MissionClass); if (constructor == null) { DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!", - contentPackage: element.ContentPackage); + contentPackage: ConfigElement.ContentPackage); } - InitProjSpecific(element); +#if CLIENT + ParseConfigElementClient(ConfigElement, variantOf); +#endif } private Type FindMissionClass(ContentXElement element) @@ -476,8 +493,6 @@ namespace Barotrauma } return constructor; } - - partial void InitProjSpecific(ContentXElement element); public bool IsAllowed(Location from, Location to) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 5037081ce..941b7c4dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -64,6 +64,8 @@ namespace Barotrauma foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { + if (GameMain.NetworkMember == null && monsterElement.GetAttributeBool("multiplayeronly", false)) { continue; } + speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 1c766a7fc..3ad3effe1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -14,8 +14,8 @@ namespace Barotrauma private readonly List items = new List(); private readonly Dictionary statusEffectOnApproach = new Dictionary(); - //string = filename, point = min,max - private readonly HashSet> monsterPrefabs = new HashSet>(); + //key = monster to spawn, point = min,max + private readonly List> monsterPrefabs = new List>(); private float itemSpawnRadius = 800.0f; private readonly float approachItemsRadius = 1000.0f; @@ -70,6 +70,8 @@ namespace Barotrauma foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { + if (GameMain.NetworkMember == null && monsterElement.GetAttributeBool("multiplayeronly", false)) { continue; } + Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index d9cd9964a..c610682ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -300,7 +300,9 @@ namespace Barotrauma return; } Submarine refSub = GetReferenceSub(acceptRemoteControlledSubs: true); - if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null) + //randomly choose which main sub to spawn the monsters around if there's 2 player subs (e.g. in the PvP mode) + if (Submarine.MainSubs.Length == 2 && + Submarine.MainSubs[1] is { Info.Type: SubmarineType.Player }) { refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index 4e4e4bba4..129fa3d25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -89,7 +89,19 @@ namespace Barotrauma private static async Task GetSteamAuthTicket() { - var authTicket = await SteamManager.GetAuthTicketForGameAnalyticsConsent(); + var authTicketTask = SteamManager.GetAuthTicketForGameAnalyticsConsent(); + + // Add a timeout to prevent the game from freezing indefinitely + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10)); + + var completedTask = await Task.WhenAny(authTicketTask, timeoutTask); + if (completedTask == timeoutTask) + { + throw new TimeoutException("Timed out while trying to retrieve Steamworks authentication ticket for GameAnalytics."); + } + + var authTicket = await authTicketTask; + return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 } ? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex : throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 7bbcf7418..72ab4b6f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -690,7 +690,7 @@ namespace Barotrauma foreach (Item containedItem in character.Inventory.AllItemsMod) { //only put into containers that draw the inventory (not ones with a hidden inventory like circuit boxes!) - if (containedItem.OwnInventory?.Container is { DrawInventory: true } && + if (containedItem.OwnInventory?.Container is { DrawInventory: true } container && container.IsAccessible() && containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots)) { break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index a16e44f2d..daa20659a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -469,7 +469,8 @@ namespace Barotrauma foreach (var mission in currentLocation.AvailableMissions) { //if the mission isn't shown in menus, it cannot be selected by the player -> must be something that is supposed to be automatically selected - if (!mission.Prefab.ShowInMenus) + //side objectives are also automatically selected + if (!mission.Prefab.ShowInMenus || mission.Prefab.IsSideObjective) { currentLocation.SelectMission(mission); } @@ -1429,18 +1430,18 @@ namespace Barotrauma map = null; } - public int NumberOfMissionsAtLocation(Location location) + public int NumberOfSelectableMissionsAtLocation(Location location) { - return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location)) ?? 0; + return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location) && !m.Prefab.IsSideObjective) ?? 0; } public void CheckTooManyMissions(Location currentLocation, Client sender) { foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation))) { - if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount) + if (NumberOfSelectableMissionsAtLocation(location) > Settings.TotalMaxMissionCount) { - DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); + DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfSelectableMissionsAtLocation(location)}. Deselecting extra missions."); foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList()) { currentLocation.DeselectMission(mission); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index ed1c3c05b..e00bf540b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -30,11 +30,23 @@ namespace Barotrauma : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionTypes, difficultyLevel: GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty); + float difficulty = GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty; + var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionTypes, difficultyLevel: difficulty); + if (mission == null) + { + DebugConsole.AddWarning( + $"Could not find any missions matching the mission types {string.Join(", ", missionTypes.Select(m => m.Value))} " + + $"and the difficulty {difficulty}. Ignoring the difficulty requirement..."); + mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionTypes); + } if (mission != null) { missions.Add(mission); } + else + { + DebugConsole.AddWarning($"Could not find any missions matching the mission types {string.Join(", ", missionTypes.Select(m => m.Value))}."); + } } protected static IEnumerable ValidateMissionPrefabs(IEnumerable missionPrefabs, Dictionary missionClasses) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index b54fac340..ffe92f4fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -741,7 +741,7 @@ namespace Barotrauma var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage); if (missionsToShow.Count() > 1) { - string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name)); + string joinedMissionNames = string.Join(", ", missions.Where(static m => m.Prefab.ShowInMenus).Select(static m => m.Name)); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 291903c48..98b9bd60a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -226,6 +226,14 @@ namespace Barotrauma return false; } + public bool IsSlotEmpty(InvSlotType limbSlot) + { + for (int i = 0; i < slots.Length; i++) + { + if (SlotTypes[i] == limbSlot && slots[i].Empty()) { return true; } + } + return false; + } /// /// Can the item be put in the inventory in a slot of the specified type (i.e. is there a suitable free slot or a stack the item can be put in). @@ -438,7 +446,8 @@ namespace Barotrauma } int placedInSlot = -1; - foreach (InvSlotType allowedSlot in allowedSlots) + //order by whether the slot is empty, i.e. try putting in free slots first before trying to unequip items from occupied slots + foreach (InvSlotType allowedSlot in allowedSlots.OrderBy(slotType => IsSlotEmpty(slotType) ? 0 : 1)) { if (allowedSlot.HasFlag(InvSlotType.RightHand) && character.AnimController.GetLimb(LimbType.RightHand) == null) { continue; } if (allowedSlot.HasFlag(InvSlotType.LeftHand) && character.AnimController.GetLimb(LimbType.LeftHand) == null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 00e85a1c2..be801e1eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -87,6 +87,13 @@ namespace Barotrauma.Items.Components "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] public DirectionType ForceDockingDirection { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Was the docking port docked at the end of the previous round.")] + public bool WasDocked + { + get; + set; + } + public DockingPort DockingTarget { get; private set; } /// @@ -280,6 +287,9 @@ namespace Barotrauma.Items.Components OnDocked?.Invoke(); OnDocked = null; + + WasDocked = true; + DockingTarget.Docked = true; } public void Lock(bool isNetworkMessage, bool applyEffects = true, bool moveSubs = true) @@ -988,6 +998,8 @@ namespace Barotrauma.Items.Components Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); obstructedWayPointsDisabled = false; + WasDocked = false; + DockingTarget.WasDocked = false; DockingTarget.Undock(); DockingTarget = null; @@ -1052,6 +1064,16 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + //PRETTY HACKY: + //the docking port was docked on the previous round, but not any more - + //must mean that whatever it was docked to (e.g. some enemy sub or respawn shuttle) no longer exists + //let's send an "on_undock" signal so circuits can react to the undocking that never "actually" happened + if (!docked && WasDocked) + { + item.SendSignal("1", "on_undock"); + WasDocked = false; + } + dockingCooldown -= deltaTime; if (DockingTarget == null) { @@ -1208,19 +1230,21 @@ namespace Barotrauma.Items.Components } } - if (!item.linkedTo.Any()) { return; } - - List linked = new List(item.linkedTo); - foreach (MapEntity entity in linked) - { - if (!(entity is Item linkedItem)) { continue; } - - var dockingPort = linkedItem.GetComponent(); - if (dockingPort != null) + if (item.linkedTo.Any()) + { + List linked = new List(item.linkedTo); + foreach (MapEntity entity in linked) { - Dock(dockingPort); - } + if (entity is not Item linkedItem) { continue; } + + var dockingPort = linkedItem.GetComponent(); + if (dockingPort != null) + { + Dock(dockingPort); + } + } } + } public override void ReceiveSignal(Signal signal, Connection connection) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 0b01d7546..dacf4103a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -154,6 +154,13 @@ namespace Barotrauma.Items.Components } } + [Serialize("0,0", IsPropertySaveable.Yes)] + public Point DisallowAttachingOverSize + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] public bool AttachedByDefault { @@ -496,13 +503,19 @@ namespace Barotrauma.Items.Components Vector2 diff = new Vector2( (heldHand.SimPosition.X - arm.SimPosition.X) / 2f, (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f); - item.SetTransform(heldHand.SimPosition + diff, 0.0f); + + //we have forced the item to be in the same sub as the dropper above, + //and are placing it to the position of the hands in "local" coordinates + //which may be outside the sub if the character is e.g. standing half-way through the airlock + // -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space, + // or it will end up in a weird state and seemingly disappear + item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine); } else { - item.SetTransform(picker.SimPosition, 0.0f); - } - } + item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine); + } + } } picker.Inventory.RemoveItem(item); @@ -621,17 +634,34 @@ namespace Barotrauma.Items.Components if (disallowAttachingOverTags.Any() || !AllowAttachInsideDoors) { var connectedHulls = item.CurrentHull?.GetConnectedHulls(includingThis: true, searchDepth: 5, ignoreClosedGaps: true); - Vector2 size = item.Rect.Size.ToVector2() / 2; + + Vector2 size = DisallowAttachingOverSize == Point.Zero ? + item.Rect.Size.ToVector2() : + DisallowAttachingOverSize.ToVector2() * item.Scale; + size /= 2f; + foreach (Item otherItem in Item.ItemList) { if (otherItem == item || otherItem.body is { BodyType: BodyType.Dynamic, Enabled: true }) { continue; } if (connectedHulls != null && !connectedHulls.Contains(otherItem.CurrentHull)) { continue; } - if (disallowAttachingOverTags.None(tag => otherItem.HasTag(tag)) && + if (disallowAttachingOverTags.None(otherItem.HasTag) && (otherItem.GetComponent() == null || AllowAttachInsideDoors)) { continue; } Rectangle worldRect = otherItem.WorldRect; + + if (otherItem.GetComponent() is Holdable otherHoldable) + { + if (!otherHoldable.attached) { continue; } + if (otherHoldable.DisallowAttachingOverSize != Point.Zero) + { + Vector2 scaledSize = otherHoldable.DisallowAttachingOverSize.ToVector2() * item.Scale; + worldRect = new Rectangle( + otherItem.WorldPosition.ToPoint() - new Point((int)(scaledSize.X / 2), (int)(-scaledSize.Y / 2)), + scaledSize.ToPoint()); + } + } if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; } if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; } tempOverlappingItems.Add(otherItem); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 7610321f2..d4d45f120 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -539,8 +539,8 @@ namespace Barotrauma.Items.Components } if (targetEntity != null) { - 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); + ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, attackMultiplier: damageMultiplier); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, attackMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 586b7f1f7..8e156bb54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -922,7 +922,8 @@ namespace Barotrauma.Items.Components } } - public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f) + /// Multiplier used on afflictions caused by the status effects, except ones that have been configured to not be affected by attack multipliers. + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float attackMultiplier = 1.0f) { if (statusEffectLists == null) { return; } @@ -934,7 +935,7 @@ namespace Barotrauma.Items.Components { if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; } if (user != null) { effect.SetUser(user); } - effect.AfflictionMultiplier = afflictionMultiplier; + effect.AttackMultiplier = attackMultiplier; var c = character; if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { @@ -942,7 +943,7 @@ namespace Barotrauma.Items.Components c = user; } item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); - effect.AfflictionMultiplier = 1.0f; + effect.AttackMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } //if any of the effects reduce the item's condition, set the user for OnBroken effects as well diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index ea55a8fe6..d0c8b38ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -832,7 +832,7 @@ namespace Barotrauma.Items.Components public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { - if (skills.Length == 0) { return 1.0f; } + if (skills.Length == 0) { return 0.5f; } if (character == null) { return 0.0f; } float minDegreeOfSuccess = 1.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/InheritConditionFromLinkedWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/InheritConditionFromLinkedWall.cs new file mode 100644 index 000000000..63e8d3664 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/InheritConditionFromLinkedWall.cs @@ -0,0 +1,46 @@ +using Barotrauma.Extensions; +using System; +using System.Collections.Generic; + +namespace Barotrauma.Items.Components +{ + /// + /// Makes the item inherit the condition from a linked wall or multiple - or in other words, makes it essentially treat the health of the wall as its own health. + /// The wall section with the most damage determines the condition (i.e. the item will be fully broken if there's at least one fully broken wall section). + /// + class InheritConditionFromLinkedWall(Item item, ContentXElement element) : ItemComponent(item, element) + { + private readonly List linkedWalls = []; + + public override void OnMapLoaded() + { + foreach (var linkedTo in item.linkedTo) + { + if (linkedTo is Structure structure && + structure.HasBody) + { + linkedWalls.Add(structure); + structure.OnHealthChanged += (_, _) => UpdateCondition(); + } + } + if (linkedWalls.None()) + { + DebugConsole.AddWarning($"The item {item.Name} ({item.Prefab.Identifier}) is not linked to any walls with a physics body. The {nameof(InheritConditionFromLinkedWall)} component will do nothing."); + } + + } + + private void UpdateCondition() + { + float lowestHealthPercent = 1.0f; + foreach (var wall in linkedWalls) + { + foreach (var section in wall.Sections) + { + lowestHealthPercent = Math.Min(lowestHealthPercent, 1.0f - section.damage / wall.MaxHealth); + } + } + item.Condition = item.MaxCondition * lowestHealthPercent; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index dab4a647c..8db49d25b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -136,6 +136,7 @@ namespace Barotrauma.Items.Components item.CurrentHull.GetLinkedHulls(linkedHulls, includeHiddenHulls: true); foreach (var linkedHull in linkedHulls) { + if (linkedHull == item.CurrentHull) { continue; } hullWaterVolume += linkedHull.WaterVolume; totalHullVolume += linkedHull.Volume; } @@ -148,7 +149,7 @@ namespace Barotrauma.Items.Components if (!IsActive || Disabled) { return; } if (flowPercentage <= 0f && item.CurrentHull.WaterVolume <= 0f) { return; } - float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); + float powerFactor = Math.Min(PowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); currFlow = flowPercentage / 100.0f * MaxFlow * powerFactor; if (item.GetComponent() is { IsTinkering: true } repairable) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index b5a84491f..17e2246b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -247,9 +247,13 @@ namespace Barotrauma.Items.Components } } + bool fissionRateControlledBySignals = signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1; + bool turbineOutputRateControlledBySignals = signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1; + //rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds //(unless the reactor is being operated by a player) - if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true }) + if (GameMain.GameSession is { RoundDuration: < 5 } && lastUser is not { IsPlayer: true } && PowerOn && AutoTemp && + !fissionRateControlledBySignals && !turbineOutputRateControlledBySignals) { UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f)); } @@ -263,7 +267,7 @@ namespace Barotrauma.Items.Components float maxPowerOut = GetMaxOutput(); - if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1) + if (fissionRateControlledBySignals) { TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f); #if CLIENT @@ -274,7 +278,7 @@ namespace Barotrauma.Items.Components { signalControlledTargetFissionRate = null; } - if (signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1) + if (turbineOutputRateControlledBySignals) { TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 7e5073bd1..52df29696 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -641,8 +641,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < hits.Count; i++) { var h = hits[i]; - item.SetTransform(h.Point, rotation); - item.Submarine = h.Submarine; + item.SetTransform(h.Point, rotation, forceSubmarine: h.Submarine); item.UpdateTransform(); if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index 031fee432..6ae6743a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -103,7 +103,7 @@ namespace Barotrauma.Items.Components var inputBuilder = ImmutableArray.CreateBuilder(); var outputBuilder = ImmutableArray.CreateBuilder(); - foreach (Connection conn in Item.Connections) + foreach (Connection conn in Item.Connections.OrderBy(static c => c.DisplayOrder)) { if (conn.IsOutput) { @@ -236,9 +236,7 @@ namespace Barotrauma.Items.Components cloneNode.ReplaceAllConnectionLabelOverrides(origNode.ConnectionLabelOverrides); } - if (!clonedContainedItems.Any()) { return; } - - foreach (var origComp in original.Components) + foreach (CircuitBoxComponent origComp in original.Components) { if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; } var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource); @@ -661,6 +659,7 @@ namespace Barotrauma.Items.Components } wire.From.Connection.CircuitBoxConnections.Remove(wire.To); + wire.To.Connection.CircuitBoxConnections.Remove(wire.From); if (wire.From is CircuitBoxInputConnection input) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 7d0e38cc8..e14929804 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -17,6 +17,8 @@ namespace Barotrauma.Items.Components //how many wires can be linked to this connection in total public readonly int MaxWires = 5; + public readonly int DisplayOrder; + public readonly string Name; private readonly LocalizedString _displayName; public LocalizedString DisplayName @@ -92,7 +94,7 @@ namespace Barotrauma.Items.Components return "Connection (" + item.Name + ", " + Name + ")"; } - public Connection(ContentXElement element, ConnectionPanel connectionPanel, IdRemap idRemap) + public Connection(ContentXElement element, int connectionIndex, ConnectionPanel connectionPanel, IdRemap idRemap, bool isItemSwap) { #if CLIENT @@ -117,25 +119,44 @@ namespace Barotrauma.Items.Components IsOutput = element.Name.ToString() == "output"; Name = element.GetAttributeString("name", IsOutput ? "output" : "input"); + int displayOrder; + if (element.GetAttribute("displayorderoverride") is not { } displayOrderAttr) + { + var sameElements = connectionPanel.Connections.Where(c => c.IsOutput == IsOutput); + displayOrder = !sameElements.Any() ? 0 : sameElements.Max(static c => c.DisplayOrder) + 1; + } + else + { + displayOrder = displayOrderAttr.GetAttributeInt(0); + } + + DisplayOrder = displayOrder; + string displayNameTag = "", fallbackTag = ""; //if displayname is not present, attempt to find it from the prefab if (element.GetAttribute("displayname") == null) { foreach (var subElement in item.Prefab.ConfigElement.Elements()) { - if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; } - + if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; } + int prefabConnectionIndex = 0; foreach (XElement connectionElement in subElement.Elements()) { string prefabConnectionName = connectionElement.GetAttributeString("name", null); + if (prefabConnectionName.IsNullOrEmpty()) { continue; } + string[] aliases = connectionElement.GetAttributeStringArray("aliases", Array.Empty()); - if (prefabConnectionName == Name || aliases.Contains(Name)) + if (prefabConnectionName == Name || aliases.Contains(Name) || + //when swapping items, we move wires based on the order of the connections, not the names + //= we should find a connection based on the index if the name doesn't match + (isItemSwap && connectionIndex == prefabConnectionIndex)) { displayNameTag = connectionElement.GetAttributeString("displayname", ""); fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", ""); } + prefabConnectionIndex++; } - } + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 238f9f47c..5fddc96c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -78,10 +78,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString()) { case "input": - Connections.Add(new Connection(subElement, this, IdRemap.DiscardId)); + Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false)); break; case "output": - Connections.Add(new Connection(subElement, this, IdRemap.DiscardId)); + Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false)); break; } } @@ -293,10 +293,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString()) { case "input": - loadedConnections.Add(new Connection(subElement, this, idRemap)); + loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap)); break; case "output": - loadedConnections.Add(new Connection(subElement, this, idRemap)); + loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap)); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionSelectorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionSelectorComponent.cs index 041853392..654bfd37a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionSelectorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionSelectorComponent.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Networking; +using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +8,7 @@ namespace Barotrauma.Items.Components; /// /// Base class for signal components that can select between input/output connections (e.g. multiplexer and demultiplexer components) /// -abstract class ConnectionSelectorComponent : ItemComponent +abstract partial class ConnectionSelectorComponent : ItemComponent, IServerSerializable { protected int selectedConnectionIndex; protected string selectedConnectionIndexStr; @@ -22,6 +23,8 @@ abstract class ConnectionSelectorComponent : ItemComponent get { return selectedConnectionIndex; } set { + int prevIndex = selectedConnectionIndex; // store original, so we know if the state has changed and can sync it in MP + selectedConnectionIndex = Math.Max(0, value); //don't clamp until we've determined how many connections the item has //(can't be done until the connection panel component has been loaded too) @@ -31,6 +34,11 @@ abstract class ConnectionSelectorComponent : ItemComponent } selectedConnectionName = GetConnectionName(selectedConnectionIndex); selectedConnectionIndexStr = selectedConnectionIndex.ToString(); + + if (prevIndex != selectedConnectionIndex) + { + OnStateChanged(); + } } } @@ -55,6 +63,8 @@ abstract class ConnectionSelectorComponent : ItemComponent { } + partial void OnStateChanged(); + protected abstract string GetConnectionName(int connectionIndex); /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 07f67e3e8..c8a3a4ec2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -63,10 +63,7 @@ namespace Barotrauma.Items.Components /// This can be used to make them additionally work the other way around, periodically getting the current value of the property from the item and refreshing the UI. /// public float GetValueInterval { get; set; } = -1.0f; - -#if CLIENT public float GetValueTimer; -#endif public string Name => "CustomInterfaceElement"; @@ -248,7 +245,7 @@ namespace Barotrauma.Items.Components ciElement.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal == ciElement.ContinuousSignal); } customInterfaceElementList.Add(ciElement); - IsActive |= ciElement.ContinuousSignal; + IsActive |= ciElement.ContinuousSignal || ciElement.GetValueInterval > 0.0f; } InitProjSpecific(); @@ -348,13 +345,10 @@ namespace Barotrauma.Items.Components //make sure the clients know about the states of the checkboxes and text fields if (customInterfaceElementList.Any()) { - if (item.FullyInitialized) + CoroutineManager.Invoke(() => { - CoroutineManager.Invoke(() => - { - if (!item.Removed) { item.CreateServerEvent(this); } - }, delay: 0.1f); - } + if (item.FullyInitialized && !item.Removed) { item.CreateServerEvent(this); } + }, delay: 0.1f); } #endif } @@ -418,6 +412,16 @@ namespace Barotrauma.Items.Components { foreach (CustomInterfaceElement ciElement in customInterfaceElementList) { + if (ciElement.GetValueInterval > 0.0f) + { + ciElement.GetValueTimer -= deltaTime; + if (ciElement.GetValueTimer <= 0.0f) + { + SetSignalToPropertyValue(ciElement); + ciElement.GetValueTimer = ciElement.GetValueInterval; + } + } + if (!ciElement.ContinuousSignal && ciElement.PropertyName != "Voltage") { continue; } //TODO: allow changing output when a tickbox is not selected if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index ed5b5916c..e0104a213 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Items.Components } } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates. 0 = not at all, 1 = alternates between full brightness and off.")] public float PulseAmount { get { return pulseAmount; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 56d1bfbed..d5d7678c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -268,7 +268,7 @@ namespace Barotrauma.Items.Components public float RotationSpeedHighSkill { get; private set; } [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A)."), - Editable] + Editable(TransferToSwappedItem = true)] public Color HudTint { get; set; } [Header(localizedTextTag: "sp.turret.AutoOperate.propertyheader")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c8bc59422..b250f2bf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -462,10 +462,25 @@ namespace Barotrauma } } - public float ImpactTolerance => Prefab.ImpactTolerance; + private float impactTolerance; + [Serialize(0.0f, IsPropertySaveable.No), ConditionallyEditable(ConditionallyEditable.ConditionType.ReceivesSubmarineImpacts, MinValueFloat = 0, MaxValueFloat = 100)] + public float ImpactTolerance + { + get { return impactTolerance; } + set { impactTolerance = Math.Max(value, 0.0f); } + } - public float ImpactDamage => Prefab.ImpactDamage; - public float ImpactDamageProbability => Prefab.ImpactDamageProbability; + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of damage the item takes from impacts. Acts as a multiplier on the strength of the impact. Note that ImpactTolerance must be set for impacts to register."), + ConditionallyEditable(ConditionallyEditable.ConditionType.ReceivesSubmarineImpacts, MinValueFloat = 0, MaxValueFloat = 100)] + public float ImpactDamage { get; set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "Probability for impacts to register. Defaults to 1. Note that ImpactTolerance must also be set for impacts to register."), + ConditionallyEditable(ConditionallyEditable.ConditionType.ReceivesSubmarineImpacts, MinValueFloat = 0, MaxValueFloat = 1)] + public float ImpactDamageProbability { get; set; } + + public const float SubmarineImpactCooldown = 0.1f; + + public double LastSubmarineImpactTime; public float InteractDistance => Prefab.InteractDistance; @@ -1559,7 +1574,7 @@ namespace Barotrauma if (!updateableComponents.Contains(component)) { updateableComponents.Add(component); - this.isActive = true; + this.IsActive = true; } } }; @@ -1650,7 +1665,19 @@ namespace Barotrauma contained.Container = null; } - public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true) + /// + /// Sets the position and rotation of the item, and its physics body if it has one. + /// + /// Position in simulation units. + /// Rotation in radians + /// Should the hull the item is inside be immediately updated? Generally only useful to set to false + /// for performance reasons when finding the hull is unnecessary (e.g. if it's being forced to something after setting the transform). + /// Should the previous transform of the item be immediately set to the new one? + /// The previous transform is used to interpolate draw positions/rotations, and you should generally only set this to false if + /// you're trying to simulate movement instead of simply teleporting the item somewhere. + /// If you know the position is in a specific sub's coordinate space and want to ensure the item + /// is still considered to be in that sub even if the transform ended up outside hulls. + public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true, Submarine forceSubmarine = null) { if (!MathUtils.IsValid(simPosition)) { @@ -1688,6 +1715,7 @@ namespace Barotrauma rect.Y = (int)MathF.Round(displayPos.Y + rect.Height / 2.0f); if (findNewHull) { FindHull(); } + if (forceSubmarine != null) { Submarine = forceSubmarine; } } /// @@ -1859,7 +1887,7 @@ namespace Barotrauma if (newRootContainer != RootContainer) { RootContainer = newRootContainer; - isActive = true; + IsActive = true; foreach (Item containedItem in ContainedItems) { containedItem.RefreshRootContainer(); @@ -2374,12 +2402,16 @@ namespace Barotrauma } } - private bool isActive = true; + /// + /// Inactive items are not updated. Note that actions such as dropping can reactivate the item, and that the item can go inactive by itself if it no longer needs updating; + /// + public bool IsActive = true; + public bool IsInRemoveQueue; public override void Update(float deltaTime, Camera cam) { - if (!isActive || IsLayerHidden || IsInRemoveQueue) { return; } + if (!IsActive || IsLayerHidden || IsInRemoveQueue) { return; } if (impactQueue != null) { @@ -2545,7 +2577,7 @@ namespace Barotrauma #if CLIENT positionBuffer.Clear(); #endif - isActive = false; + IsActive = false; } } @@ -2706,7 +2738,7 @@ namespace Barotrauma impactQueue.Enqueue(impact); } - isActive = true; + IsActive = true; return true; } @@ -3496,7 +3528,7 @@ namespace Barotrauma if (body != null) { - isActive = true; + IsActive = true; body.Enabled = true; body.PhysEnabled = true; body.ResetDynamics(); @@ -3636,7 +3668,7 @@ namespace Barotrauma item.body.Enabled = item.body.PhysEnabled = isFirst; if (isFirst) { - item.isActive = true; + item.IsActive = true; item.body.ResetDynamics(); } } @@ -4401,13 +4433,18 @@ namespace Barotrauma { foreach (var connection in thisConnectionPanel.Connections) { - var newConnection = newConnectionPanel.Connections.FirstOrDefault(c => c.Name == connection.Name); - if (newConnection == null) { continue; } foreach (var wire in connection.Wires) { - int connectionIndex = wire.Connections.IndexOf(connection); + int wireConnectionIndex = wire.Connections.IndexOf(connection); wire.RemoveConnection(this); - wire.Connect(newConnection, connectionIndex, addNode: false); + int thisConnectionIndex = connection.ConnectionPanel.Connections.IndexOf(connection); + if (thisConnectionIndex < 0 || thisConnectionIndex >= newConnectionPanel.Connections.Count) + { + DebugConsole.AddWarning($"Failed to move a wire from the connection {connection.Name} when swapping the item {Name} with {newItem.Name}. The new item probably does not have the same number of connections as the previous one."); + continue; + } + Connection newConnection = newConnectionPanel.Connections[thisConnectionIndex]; + wire.Connect(newConnection, wireConnectionIndex, addNode: false); newConnection.ConnectWire(wire); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index c8357fc58..c340efab6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -813,20 +813,6 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DamagedByMonsters { get; private set; } - private float impactTolerance; - [Serialize(0.0f, IsPropertySaveable.No)] - public float ImpactTolerance - { - get { return impactTolerance; } - set { impactTolerance = Math.Max(value, 0.0f); } - } - - [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of damage the item takes from impacts. Acts as a multiplier on the strength of the impact. Note that ImpactTolerance must be set for impacts to register.")] - public float ImpactDamage { get; set; } - - [Serialize(1.0f, IsPropertySaveable.No, description: "Probability for impacts to register. Defaults to 1. Note that ImpactTolerance must also be set for impacts to register.")] - public float ImpactDamageProbability { get; set; } - [Serialize(false, IsPropertySaveable.No, "If true, submarine impacts will trigger OnImpact effects. Only applies to items with a null or non-dynamic physics body - items with dynamic bodies always react to impacts.")] public bool ReceiveSubmarineImpacts { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index ea8a21405..5c11eebb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -404,7 +404,7 @@ namespace Barotrauma return; } - DamageCharacters(worldPosition, Attack, force, damageSource, attacker); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker, displayRange); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -465,12 +465,12 @@ namespace Barotrauma partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) + private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, float range) { - if (attack.Range <= 0.0f) { return; } + if (range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't - float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); + float broadRange = Math.Max(range * 10.0f, 10000.0f); foreach (Character c in Character.CharacterList) { @@ -518,7 +518,7 @@ namespace Barotrauma float limbRadius = limb.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); - if (dist > attack.Range) { continue; } + if (dist > range) { continue; } float distFactor = DistanceFalloff ? diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 9f32878dd..c2badfe3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -371,7 +371,7 @@ namespace Barotrauma if (!IsInDamageRange(c, DamageRange)) { continue; } //GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between) - if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange + FlameHeight) + if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, maxDistance: 10000.0f, minimumGapOpenness: Structure.LargeGapOpenness) > size.X + DamageRange + FlameHeight) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 1bba2e53f..1c193d983 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1133,54 +1133,72 @@ namespace Barotrauma } /// - /// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls. - /// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found. + /// Used in /// - public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0) + private static readonly Dictionary cachedDistances = []; + /// + /// Used in + /// + private static readonly PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue = new PriorityQueue<(Hull hull, Vector2 pos), float>(); + + /// + /// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls. + /// Uses a Dijkstra's algorithm to find the shortest path. + /// + /// The gap's openness must be larger than or equal to this to be considered valid for the path. + public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0, float minimumGapOpenness = 0.5f) { - return GetApproximateHullDistance(startPos, endPos, new HashSet(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor); - } + cachedDistances.Clear(); + priorityQueue.Clear(); - private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0) - { - if (distance >= maxDistance) { return float.MaxValue; } - if (this == target) + cachedDistances[this] = 0f; + priorityQueue.Enqueue((this, startPos), 0f); + + while (priorityQueue.TryDequeue(out var current, out float currentDist)) { - return distance + Vector2.Distance(startPos, endPos); - } + Hull currentHull = current.hull; + Vector2 currentPos = current.pos; - connectedHulls.Add(this); + if (currentDist > maxDistance) { return float.MaxValue; } - foreach (Gap g in ConnectedGaps) - { - float distanceMultiplier = 1; - if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) + // If we've reached the target, add the final segment from hull to endPos + if (currentHull == targetHull) { - //gap blocked if the door is closed, and we haven't made any predictions of it opening client-side - if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) || - //OR we've predicted that the door is closed client-side - (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + return currentDist + Vector2.Distance(currentPos, endPos); + } + + foreach (Gap g in ConnectedGaps) + { + float distanceMultiplier = 1; + if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { - if (g.ConnectedDoor.OpenState < 0.1f) + //gap blocked if the door is closed, and we haven't made any predictions of it opening client-side + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) || + //OR we've predicted that the door is closed client-side + (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { - if (distanceMultiplierFromDoors <= 0) { continue; } - distanceMultiplier *= distanceMultiplierFromDoors; + if (g.ConnectedDoor.OpenState < 0.1f) + { + if (distanceMultiplierPerClosedDoor <= 0) { continue; } + distanceMultiplier *= distanceMultiplierPerClosedDoor; + } } } - } - else if (g.Open <= 0.0f) - { - continue; - } - - for (int i = 0; i < 2 && i < g.linkedTo.Count; i++) - { - if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull)) + else if (g.Open < minimumGapOpenness) { - float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance); - if (dist < float.MaxValue) + continue; + } + + for (int i = 0; i < 2 && i < g.linkedTo.Count; i++) + { + if (g.linkedTo[i] is Hull nextHull && nextHull != currentHull) { - return dist; + float newDist = currentDist + Vector2.Distance(currentPos, g.Position) * distanceMultiplier; + if (!cachedDistances.TryGetValue(nextHull, out float oldDist) || newDist < oldDist) + { + cachedDistances[nextHull] = newDist; + priorityQueue.Enqueue((nextHull, g.Position), newDist); + } } } } @@ -1274,7 +1292,7 @@ namespace Barotrauma } /// - /// Recursively find all the hulls linked to the specified hull. + /// Recursively find all the hulls linked to the specified hull, including the hull itself. /// public void GetLinkedHulls(List linkedHulls, bool includeHiddenHulls = false) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 264395a98..fc26aff4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Barotrauma { - class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity + internal partial class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { public readonly static PrefabCollection LevelParams = new PrefabCollection(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index c55e16485..637d5d2a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -378,7 +378,14 @@ namespace Barotrauma if (characters.Any()) { price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false)); - price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag))); + price *= 1f + characters.Max(c => GetMultiplierForItem(c, item)); + + float GetMultiplierForItem(Character character, ItemPrefab item) + { + return + item.Tags.Sum(tag => character.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)) + + character.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, Tags.StatIdentifierTargetAll); + } } // Price should never go below 1 mk @@ -588,7 +595,7 @@ namespace Barotrauma public Location(Vector2 mapPosition, int? zone, Identifier? biomeId, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable existingLocations = null) { Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, biomeId, requireOutpost); - CreateRandomName(Type, rand, existingLocations); + AssignRandomName(Type, rand, existingLocations); MapPosition = mapPosition; PortraitId = ToolBox.StringToInt(nameIdentifier.Value); Connections = new List(); @@ -1210,7 +1217,7 @@ namespace Barotrauma HireManager.AvailableCharacters = hireableCharacters.ToList(); } - private void CreateRandomName(LocationType type, Random rand, IEnumerable existingLocations) + public void AssignRandomName(LocationType type, Random rand, IEnumerable existingLocations) { if (!type.ForceLocationName.IsEmpty) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 785f729c6..308234df4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -735,7 +735,7 @@ namespace Barotrauma Location startLocation = Locations.MinBy(l => l.MapPosition.X); if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType)) { - startLocation.ChangeType(campaign, startLocationType, createStores: false); + mapLocationTypeGenerator.ChangeLocationTypeAndName(campaign, startLocation, startLocationType); mapLocationTypeGenerator.AddToFilled(startLocation); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapLocationTypeGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapLocationTypeGenerator.cs index 7003b9848..9b254c52a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapLocationTypeGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapLocationTypeGenerator.cs @@ -155,13 +155,17 @@ namespace Barotrauma return filledLocations.Contains(location); } - public static void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType) + public void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType) { location.ChangeType(campaign, suitableLocationType, createStores: false, unlockInitialMissions: false); if (!suitableLocationType.ForceLocationName.IsEmpty) { location.ForceName(suitableLocationType.ForceLocationName); } + else + { + location.AssignRandomName(location.Type, Rand.GetRNG(Rand.RandSync.ServerAndClient), existingLocations: map.Locations); + } } public void AssignForcedBiomeGateTypes(IEnumerable gateLocations) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 61a96bca8..64f01bb58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -823,6 +823,7 @@ namespace Barotrauma ItemPrefab itemPrefab = ItemPrefab.Find(name, identifier); if (itemPrefab != null) { + DebugConsole.AddWarning($"Could not find a structure with the identifier {identifier}, but there's a matching item with the identifier. Converting to an item."); t = typeof(Item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 3d1a11195..8339e6c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -53,6 +53,16 @@ namespace Barotrauma const float LeakThreshold = 0.1f; const float BigGapThreshold = 0.7f; + /// + /// How open the gap on a partially broken wall section is at most (when it's below , after which it lerps up to ). + /// + public const float SmallGapOpenness = 0.35f; + + /// + /// How open the gap on a fully broken wall section is. + /// + public const float LargeGapOpenness = 0.75f; + public override ContentPackage ContentPackage => Prefab?.ContentPackage; #if CLIENT @@ -64,6 +74,9 @@ namespace Barotrauma private static Explosion explosionOnBroken; + public delegate void OnHealthChangedHandler(Character attacker, float damage); + public OnHealthChangedHandler OnHealthChanged; + [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)] public bool Indestructible { @@ -1332,11 +1345,11 @@ namespace Barotrauma float gapOpen = 0; if (damageRatio > BigGapThreshold) { - gapOpen = MathHelper.Lerp(0.35f, 0.75f, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio)); + gapOpen = MathHelper.Lerp(SmallGapOpenness, LargeGapOpenness, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio)); } else if (damageRatio > LeakThreshold) { - gapOpen = MathHelper.Lerp(0f, 0.35f, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio)); + gapOpen = MathHelper.Lerp(0f, SmallGapOpenness, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio)); } gap.Open = gapOpen; @@ -1355,16 +1368,20 @@ namespace Barotrauma Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth); HasDamage = Sections.Any(s => s.damage > 0.0f); - if (attacker != null && damageDiff != 0.0f) + if (damageDiff != 0.0f) { - HumanAIController.StructureDamaged(this, damageDiff, attacker); - OnHealthChangedProjSpecific(attacker, damageDiff); - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + OnHealthChanged?.Invoke(attacker, damageDiff); + if (attacker != null) { - if (damageDiff < 0.0f) + HumanAIController.StructureDamaged(this, damageDiff, attacker); + OnHealthChangedProjSpecific(attacker, damageDiff); + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill, - -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage); + if (damageDiff < 0.0f) + { + attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill, + -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage); + } } } } @@ -1775,9 +1792,9 @@ namespace Barotrauma //3. not found, attempt to find a prefab that uses the previous name as an identifier if (prefab == null) { prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; } } - else + else if (StructurePrefab.Prefabs.TryGet(identifier, out StructurePrefab structurePrefab)) { - prefab = MapEntityPrefab.Find(null, identifier) as StructurePrefab; + prefab = structurePrefab; } return prefab; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 45df0b97f..64f547fa3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1400,6 +1400,8 @@ namespace Barotrauma if (item.Submarine != this) { continue; } var pump = item.GetComponent(); if (pump == null || item.CurrentHull == null) { continue; } + //if the pump has no connection panel, it must be something else than a ballast pump (e.g. a weak point which uses a pump component to pump water in) + if (item.GetComponent() == null) { continue; } if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } pump.FlowPercentage = 0.0f; ballastHulls.Add(item.CurrentHull); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 28b8af8c5..cf1dec2ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -29,7 +29,7 @@ namespace Barotrauma const float VerticalDrag = 0.05f; const float MaxDrag = 0.1f; - private const float ImpactDamageMultiplier = 10.0f; + private const float ImpactDamageMultiplier = 3.0f; //limbs with a mass smaller than this won't cause an impact when they hit the sub private const float MinImpactLimbMass = 10.0f; @@ -886,6 +886,11 @@ namespace Barotrauma } float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal); + if (wallImpact < MinCollisionImpact) { return; } + + //magic number to make wall impacts on par with monster impacts (the latter are affected by the mass of the monster) + const float WallImpactMultiplier = 3.0f; + wallImpact *= WallImpactMultiplier; ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); foreach (Submarine dockedSub in submarine.DockedTo) @@ -1070,17 +1075,20 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != submarine) { continue; } + if (Timing.TotalTimeUnpaused < item.LastSubmarineImpactTime + Item.SubmarineImpactCooldown) { continue; } if (item.body is not { BodyType: BodyType.Dynamic }) { if (!item.Prefab.ReceiveSubmarineImpacts) { continue; } item.ReceiveImpact(impact, recursive: false); + item.LastSubmarineImpactTime = Timing.TotalTimeUnpaused; } if (!item.body.Enabled || item.CurrentHull == null || item.body.Mass > impulseMagnitude) { continue; } item.body.ApplyLinearImpulse(impulse, 10.0f); item.PositionUpdateInterval = 0.0f; + item.LastSubmarineImpactTime = Timing.TotalTimeUnpaused; } float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 909aa78ba..865280f5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -113,7 +113,7 @@ namespace Barotrauma.Networking Task readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token); if (readTask is null) { return Option.None(); } - int timeOutMilliseconds = 100; + int timeOutMilliseconds = 150; for (int i = 0; i < 150; i++) { if (status is StatusEnum.ShutDown) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 87c9cda4a..41fe185a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -94,6 +94,10 @@ namespace Barotrauma } } } + if (Inventory.Owner is Character { DisabledByEvent: true }) + { + spawnedItem.IsActive = false; + } } else { @@ -363,7 +367,10 @@ namespace Barotrauma if (GameMain.Server != null) { Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); - if (client != null) GameMain.Server.SetClientCharacter(client, null); + if (client != null) + { + GameMain.Server.SetClientCharacter(client, null); + } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 38b6fc9f3..0c60db650 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -39,26 +39,6 @@ namespace Barotrauma.Networking public static int MaxEventPacketsPerUpdate = 4; - /// - /// How long the server waits for the clients to get in sync after the round has started before kicking them - /// - public static float RoundStartSyncDuration = 60.0f; - - /// - /// How long the server keeps events that everyone currently synced has received - /// - public static float EventRemovalTime = 15.0f; - - /// - /// If a client hasn't received an event that has been succesfully sent to someone within this time, they get kicked - /// - public static float OldReceivedEventKickTime = 10.0f; - - /// - /// If a client hasn't received an event after this time, they get kicked - /// - public static float OldEventKickTime = 30.0f; - /// /// Interpolates the positional error of a physics body towards zero. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 9625bd583..040321b59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -17,8 +17,8 @@ namespace Barotrauma.Networking abstract class NetworkConnection { - public static double TimeoutThreshold = 60.0; //full minute for timeout because loading screens can take quite a while - public static double TimeoutThresholdInGame = 10.0; + public static double TimeoutThresholdNotInGame => GameMain.NetworkMember?.ServerSettings?.TimeoutThresholdNotInGame ?? 60.0; //full minute for timeout because loading screens can take quite a while + public static double TimeoutThresholdInGame => GameMain.NetworkMember?.ServerSettings?.TimeoutThresholdInGame ?? 10.0; public AccountInfo AccountInfo { get; private set; } = AccountInfo.None; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs index 790ac90b7..95d929196 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs @@ -25,6 +25,6 @@ abstract class P2PConnection : NetworkConnection public void Heartbeat() { - Timeout = TimeoutThreshold; + Timeout = TimeoutThresholdNotInGame; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 36193e1e1..8918f9252 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -1045,12 +1045,77 @@ namespace Barotrauma.Networking private set; } - [Serialize(10.0f, IsPropertySaveable.Yes)] + //note: the following properties are autoinitialized because it's important for them to have sensible (non-zero) default values, + //and non-admin clients don't know the values set by the server + + [Serialize(30.0f, IsPropertySaveable.Yes)] public float MinimumMidRoundSyncTimeout { get; private set; - } + } = 30.0f; + + /// + /// How long the server waits for the clients to get in sync after the round has started before kicking them + /// + [Serialize(120.0f, IsPropertySaveable.Yes)] + public float RoundStartSyncDuration + { + get; + private set; + } = 120.0f; + + /// + /// How long the server keeps events that everyone currently synced has received + /// + [Serialize(15.0f, IsPropertySaveable.Yes)] + public float EventRemovalTime + { + get; + private set; + } = 15.0f; + + /// + /// If a client hasn't received an event that has been succesfully sent to someone within this time, they get kicked + /// + [Serialize(20.0f, IsPropertySaveable.Yes)] + public float OldReceivedEventKickTime + { + get; + private set; + } = 20.0f; + + /// + /// If a client hasn't received an event after this time, they get kicked + /// + [Serialize(40.0f, IsPropertySaveable.Yes)] + public float OldEventKickTime + { + get; + private set; + } = 40.0f; + + /// + /// Amount of seconds before connections between the server and the clients time out (i.e. if the client fails to receive messages from the server for this amount of time or vice versa, they get disconnected). + /// Used when a round is not currently running, i.e. in the lobby or in loading screens. + /// + [Serialize(60.0f, IsPropertySaveable.Yes)] + public float TimeoutThresholdNotInGame + { + get; + private set; + } = 60.0f; + + /// + /// Amount of seconds before connections between the server and the clients time out (i.e. if the client fails to receive messages from the server for this amount of time or vice versa, they get disconnected). + /// Used when a round is running. + /// + [Serialize(10.0f, IsPropertySaveable.Yes)] + public float TimeoutThresholdInGame + { + get; + private set; + } = 10.0f; private bool karmaEnabled; [Serialize(false, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 04936983b..58f0f5280 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -208,19 +208,30 @@ namespace Barotrauma isEnabled = value; try { - if (isEnabled) FarseerBody.Enabled = isPhysEnabled; else FarseerBody.Enabled = false; + FarseerBody.Enabled = isEnabled && isPhysEnabled; } catch (Exception e) { DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = " + value + " (" + isPhysEnabled + ")", e); - if (UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + UserData.GetType().ToString(), Color.Red); - if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", Color.Red); - else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", Color.Red); - if (FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", Color.Red); - + if (UserData != null) + { + DebugConsole.NewMessage("PhysicsBody UserData: " + UserData.GetType(), Color.Red); + } + if (GameMain.World.ContactManager == null) + { + DebugConsole.NewMessage("ContactManager is null!", Color.Red); + } + else if (GameMain.World.ContactManager.BroadPhase == null) + { + DebugConsole.NewMessage("Broadphase is null!", Color.Red); + } + if (FarseerBody.FixtureList == null) + { + DebugConsole.NewMessage("FixtureList is null!", Color.Red); + } if (UserData is Entity entity) { - DebugConsole.NewMessage("Entity \"" + entity.ToString() + "\" removed!", Color.Red); + DebugConsole.NewMessage("Entity \"" + entity + "\" removed!", Color.Red); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs index c187e6e6b..d93e0bc97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs @@ -37,7 +37,8 @@ sealed class ConditionallyEditable : Editable HasIntegratedButtons, IsToggleableController, HasConnectionPanel, - DeteriorateUnderStress + DeteriorateUnderStress, + ReceivesSubmarineImpacts } public bool IsEditable(ISerializableEntity entity) @@ -72,6 +73,8 @@ sealed class ConditionallyEditable : Editable => GetComponent(entity) != null, ConditionType.DeteriorateUnderStress => entity is Item repairableItem && repairableItem.Components.Any(c => c is IDeteriorateUnderStress), + ConditionType.ReceivesSubmarineImpacts + => entity is Item { Prefab.ReceiveSubmarineImpacts: true }, _ => false }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 6ab9492b9..a0cd1be2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -779,7 +779,10 @@ namespace Barotrauma /// private readonly HashSet<(Identifier affliction, float strength)> requiredAfflictions; - public float AfflictionMultiplier = 1.0f; + /// + /// Multiplier used on afflictions caused by the status effect, except ones that have been configured to not be affected by attack multipliers. + /// + public float AttackMultiplier = 1.0f; public List Afflictions { @@ -801,6 +804,12 @@ namespace Barotrauma public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>(); + /// + /// Normally using a StatusEffect to heal someone's afflictions gives an amount of medical skill relative to the amount of health the target regained. + /// This can be used to disable that behavior, in case there are items that "heal" someone without being considered medical items or something that should give medical skill. + /// + public readonly bool CanGiveMedicalSkill; + private readonly List talentTriggers; private readonly List giveExperiences; private readonly List giveSkills; @@ -906,6 +915,8 @@ namespace Barotrauma if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } + CanGiveMedicalSkill = element.GetAttributeBool(nameof(CanGiveMedicalSkill), true); + SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f); randomCondition = element.GetAttributeVector2("randomcondition", Vector2.Zero); @@ -2018,7 +2029,7 @@ namespace Barotrauma { float healthChange = targetCharacter.Vitality - prevVitality; targetCharacter.AIController?.OnHealed(healer: user, healthChange); - if (user != null) + if (user != null && CanGiveMedicalSkill) { targetCharacter.TryAdjustHealerSkill(user, healthChange); #if SERVER @@ -2689,16 +2700,6 @@ namespace Barotrauma { Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => { - if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) - { - //if the item is both pickable and wearable, try to wear it instead of picking it up - List allowedSlots = - item.GetComponents().Count() > 1 ? - new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : - new List(item.AllowedSlots); - allowedSlots.Remove(InvSlotType.Any); - character.Inventory.TryPutItem(item, null, allowedSlots); - } OnItemSpawned(item, chosenItemSpawnInfo); }); } @@ -2767,6 +2768,17 @@ namespace Barotrauma } void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) { + if (itemSpawnInfo.Equip && newItem.ParentInventory is CharacterInventory characterInventory && characterInventory.Owner is Character character) + { + //if the item is both pickable and wearable, try to wear it instead of picking it up + List allowedSlots = + newItem.GetComponents().Count() > 1 ? + new List(newItem.GetComponent()?.AllowedSlots ?? newItem.GetComponent().AllowedSlots) : + new List(newItem.AllowedSlots); + allowedSlots.Remove(InvSlotType.Any); + character.Inventory.TryPutItem(newItem, null, allowedSlots); + } + newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; if (itemSpawnInfo.InheritEventTags) { @@ -2922,7 +2934,10 @@ namespace Barotrauma targetCharacter.AIController?.OnHealed(healer: element.User, healthChange); if (element.User != null) { - targetCharacter.TryAdjustHealerSkill(element.User, healthChange); + if (element.Parent.CanGiveMedicalSkill) + { + targetCharacter.TryAdjustHealerSkill(element.User, healthChange); + } #if SERVER GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, -healthChange, 0.0f); #endif @@ -2966,12 +2981,16 @@ namespace Barotrauma afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - return afflictionMultiplier * AfflictionMultiplier; + return afflictionMultiplier; } private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool multiplyByMaxVitality) { float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); + if (affliction.AffectedByAttackMultipliers) + { + afflictionMultiplier *= AttackMultiplier; + } if (multiplyByMaxVitality) { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; @@ -3003,6 +3022,10 @@ namespace Barotrauma return affliction; } + /// + /// Register results of the afflictions that the status effect applied on the target (e.g. buffs), giving the user medical skill and indicating the treatment in the UI. + /// The effects of reducing afflictions are handled when going through the ReduceAffliction list. + /// private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affliction affliction, AttackResult result) { if (item == null) { return; } @@ -3019,12 +3042,13 @@ namespace Barotrauma if (type == ActionType.OnUse || type == ActionType.OnSuccess) { limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; - limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction); + if (CanGiveMedicalSkill) { limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction); } + } else if (type == ActionType.OnFailure) { limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; - limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction); + if (CanGiveMedicalSkill) { limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 6af96e15e..6f1406c51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -52,6 +52,7 @@ public static class Tags public static readonly Identifier ChairItem = "chair".ToIdentifier(); public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); public static readonly Identifier Thalamus = "thalamus".ToIdentifier(); + public static readonly Identifier GeneticResearchStation = "geneticresearchstation".ToIdentifier(); public static readonly Identifier IgnoreThis = "ignorethis".ToIdentifier(); public static readonly Identifier UnignoreThis = "unignorethis".ToIdentifier(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 67d2740dd..f087276a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -66,21 +66,28 @@ namespace Barotrauma private static readonly string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer"); #if OSX - //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac - public static readonly string DefaultSaveFolder = Path.Combine( + /// + /// These exist because we used to have a workaround here that set the save folder to + /// Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", "Daedalic Entertainment GmbH", "Barotrauma") + /// on Mac, because apparently LocalApplicationData returned something different than the expected path. That seems to have changed in .NET8, and now we + /// can use the same LocalApplicationData on all platforms. We however still check the old path in case someone has their saves there. + /// + public static readonly string LegacyMacSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", "Daedalic Entertainment GmbH", "Barotrauma"); -#else - //"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows - //"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux + public static string LegacyMacMultiplayerSaveFolder = Path.Combine(LegacyMacSaveFolder, "Multiplayer"); +#endif + + //C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/ on Windows + ///home/*user*/.local/share/Daedalic Entertainment GmbH/ on Linux + ///Users/*user*/Library/Application Support/Daedalic Entertainment GmbH/ on Mac public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Daedalic Entertainment GmbH", "Barotrauma"); -#endif public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer"); @@ -410,6 +417,13 @@ namespace Barotrauma files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); } +#if OSX + string legacyMacFolder = saveType == SaveType.Singleplayer ? LegacyMacSaveFolder : LegacyMacMultiplayerSaveFolder; + if (Directory.Exists(legacyMacFolder)) + { + files.AddRange(Directory.GetFiles(legacyMacFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); + } +#endif files = files.Distinct().ToList(); List saveInfos = new List(); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 184af8542..f2323febc 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,86 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.11.4.1 (Winter Update 2025) +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Wallaby, a new tier 2 Attack class submarine. Boasts an unexpectedly high amount of firepower for its size, while also providing a full set of fabrication facilities, but also includes weak points in the pipe system which can cause water hazards inside the sub. +- Added valves and weakpoints. Weakpoints are pipes that can break, causing a water hazard (which floods the submarine and is hard to pass). + - Weakpoints are repairable with a wrench. Turning off a valve makes it easier to repair, but makes any linked pumps not work while the valve is closed + - Wall weakpoints have their condition linked to a wall. This means these walls cannot be cut as a shortcut between rooms +- Added level background visuals to the sub editor's test mode to make it easier to see what the sub looks like in the actual in-game environments. +- Balancing combat mission NPCs: improved the higher-level enemy NPCs and gave them better gear. +- Made the rotation tool snap to 1-degree increments and the scale tool snap to 0.1 increments in the sub editor. Holding Shift while using the tools temporarily disables snapping. +- Added 2 more input and output connections (0 and 9) to circuit boxes to make it match the mux/demux components and use 0-indexing like other components. +- Increased the number of signal connections on relays from 2 to 5. +- You can no longer revive characters with CPR when the character has no oxygen available (e.g. by giving CPR to someone wearing a diving suit with an empty oxygen tank). Not only was this weird, it made it easy to farm medical skill by repeatedly reviving the character with CPR. + +Multiplayer: +- Fixed inventories and wallets sometimes resetting in multiplayer: more specifically, happened when a client was still connected to the server (for example in the lobby) and their character had despawned by the end of the round. +- Fixed characters sometimes suffocating or dying if loading into the round takes a long time. Now the characters don't spawn until the client has fully loaded into the round. +- Adjusted certain multiplier timeout thresholds to make errors such as "timed out", "expected old event" and "expected removed event" less frequent. Also made many of the timeout thresholds editable in the server config file (however, you should be careful when changing these values, as setting them too high may cause other issues). +- Fixed AFK setting staying enabled when a client chooses to load a campaign, causing them to load into a round with no character. +- Small improvements to the "direct join" prompt: autofocus on the text box, you can press enter to join. +- Fixed non-player teams being affected by the friendly fire setting (e.g. bandits were unable to damage monsters when friendly fire was disabled). +- Fixed input selector and output selector components' "move_input"/"move_output" signals not getting synced in multiplayer. +- Fixed a syncing issue with the timed detonators that made it possible to make them explode immediately at the start of the next round by letting the timer reach 0, and then putting an explosive inside and waiting for the next round. + +Fixes: +- Attempt to fix reported freezes at 80% in the loading screen, which failed when the game failed to fetch an authentication ticket from Steam to be used for checking whether you've given consent to collect gameplay analytics. +- Fixed explosions not showing up on the sonar if you've selected a status monitor linked to the nav terminal, instead of having directly selected the nav terminal. +- Fixed reactors automatically adjusting themselves at the start of the round when the reactor is off or controlled by signals. The intention of the rapid autoadjustment is to make sure there's no overloads when the load suddenly drops as the sub starts receiving power from the outpost, but it should not run when the reactor is off or controlled by signals. +- Fixed fire damaging characters through walls that are just slightly damaged, and overall improved the logic for calculating fire damage through gaps. +- Fixed items sometimes disappearing when you drop them standing half-way through a door way (or more specifically, when the character is considered to be in the sub, but their hands are outside hulls). +- Fixed inability to undo nudging entities with arrow keys in the sub editor. +- Fixed adding water to newly added hulls not working in the sub editor until the sub is saved and loaded. +- Fixed monsters sometimes spawning near the enemy submarine when there's a sub-vs-sub mission active (sometimes leading to the mission completing by itself when the monsters manage to take down the sub). +- Fixed items such as the "diver's remains" loot appearing on sonar despite being in a monster's inventory. More specifically, affected items with a SonarSize greater than 0. +- Fixed tooltips not refreshing on the affliction icons above the character portrait (meaning if the strength of the affliction changed, changing the description, the old description would still appear in the tooltip). +- Fixed "eliminate thalamus" mission not working in MP's mission mode unless the difficulty is set above 50. +- Fixed outpost NPCs panicking and calling security when they saw you dragging a dead pet - they should only do so if you're dragging another outpost NPC's corpse. +- Fixed purchasing genes when your inventory is full causing the genes to automatically combine with any genes in your inventory. +- Fixed NPCs that are configured to dual-wield weapons (e.g. some elite bandits) only equipping one of the weapons. +- Fixed pumps calculating the desired water level incorrectly for linked hulls when controlled via the "set_target_level" input. +- Fixed variants of husk infection always causing the "you try to scream but no sound comes out" message to pop up, even if the affliction doesn't prevent speaking (e.g. husk symbiosis). +- Fixed ability to aim with the rifle scope when you're stunned/unconscious. +- Fixed circuit boxes not saving the wires if there's no components in the box. +- Fixed wires not getting transferred when swapping e.g. a blank loader to a flak cannon loader (or more specifically, to any loader with differently named connections) in the mission mode. +- Fixed fabricators showing thalamus veins as suitable materials for items that require a wire as a material. +- Fixed assigning a specific character to operate turrets throwing a console error ("Controller not specified") when there's only one turret in the sub. +- Fixed swarm feeders, or other monsters that can latch on to characters, deattaching when you ragdoll. +- Fixed monsters becoming unable to run (or swim fast) after they've used a rope attack (e.g. latcher's tongue or the fractal guardian's harpoon). +- Fixed machines (e.g. fractal guardians, defense bot) not being immune to the "infected wound" affliction. +- Fixed outpost events that should spawn things in mines often spawning them in some random module instead (e.g. the mudraptor eggs in the "occupational hazards" event). +- Fixed portable pump working without power. +- Fixed hidden missions showing up in the scrolling text that appears at the top of the screen at the start of a round. +- Fixed traveling tradesman talent giving a larger-than-intended sale price bonus for certain items. More specifically, it gave the 20% bonus for each tag the item had. +- Fixed icons of all afflictions (even hidden ones) briefly appearing above the health bar when a character dies. +- Fixed docking port staying open if you leave a level with some non-persistent sub (e.g. respawn shuttle or an enemy sub) docked to it. + +Modding: +- Support for defining mission variants (the same way as e.g. item or character variants): allows creating variants of missions that change certain properties of the base mission without having to redefine the whole XML. +- Fixed ExplosionRadiusMultiplier not affecting the range of damage done to characters. +- Fixed items updating in preloaded characters' inventories. Meant that e.g. characters with a Disposable Diving Suit would appear to spawn with the suit already degraded or broken. +- Fixed fabricator icons using the inventory icon color even if the item has no inventory icon and uses the normal sprite (and its color) instead. +- Fixed crashing when an item fails to play a looping, streamed sound (e.g. because the maximum number of instances of that sound is already playing). Did not seem to occur in the vanilla game. +- Added support for configuring monsters as "multiplayeronly" in NestMission and MonsterMission (this worked in AbandonedOutpostMission and SalvageMission already, just hadn't been implemented here). +- Fixed StatusEffect's Equip attribute only working if the item spawns in ThisInventory (not when it spawns it e.g. in SameInventory). +- Improved reactor modding compatibility: added the tag "reactoritem" as a containable item to allow items other than fuel to be contained. +- Fixed assault rifle defining the required ammo using the identifier "assaultriflemagazine" instead of the tag "assaultrifleammo", making it difficult to add new types of ammo for the weapon. +- Fixed fabrication being a lot faster than intended (= a lot faster than the RequiredTime set in XML) if there's no skill requirements. +- Fixes characters never being considered "in water" for the first frame after they spawn, causing NotInWater effects to trigger on monsters spawning outside. +- When swapping a turret, the old turret's HudTint value is transferred to the new one. +- Fixed pump particles getting rotated incorrectly on mirrored pumps. +- Better Research Station modding compatibility: + - Added smallitem and mediumitem as containables to give more flexibility on what items can be put inside. + - Increased the number of output slots to 3 to support recipes that produce more than 1 item, and to prevent from items falling out if the input is invalid and gets moved to the output slots. +- Fixed crashing when applying an affliction's status effects causes other afflictions to apply their status effects. +- Added a new "AffectedByAttackMultipliers" property to afflictions: can be used to prevent specific afflictions from being affected by Attack's damage multipliers. Buffs aren't affected by default. +- Added a new "VitalityLossRequiredForTreatment" property to afflictions: can be used to make bots treat afflictions even if the affliction doesn't cause vitality loss. +- Added a new "CanGiveMedicalSkill" property to StatusEffects: can be used to prevent the effect from giving the user medical skill, even if it causes healing. +- Made missions' SideObjective setting more useful for modders: previously all it did was affecting which beacon and hunting grounds missions can get "spontaneously" selected for levels with beacons and hunting grounds. Now all side objectives available in a level are automatically selected, without having to manually select them in the mission selection UI. +- Set mantis's group to "mantisoid" and watcher's to "watcheroid" (doesn't do anything in the vanilla game but useful for mods). + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.10.7.2 -------------------------------------------------------------------------------------------------------------------------------------------------