diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 0bfac9a82..839d52d50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -175,11 +175,11 @@ namespace Barotrauma position += amount; } - public void ClientWrite(IWriteMessage msg) + public void ClientWrite(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { if (Character.Controlled != null && !Character.Controlled.IsDead) { return; } - msg.WriteByte((byte)ClientNetObject.SPECTATING_POS); + segmentTableWriter.StartNewSegment(ClientNetSegment.SpectatingPos); msg.WriteSingle(position.X); msg.WriteSingle(position.Y); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index c43d3f26b..56cb2ac83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -47,8 +47,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {SelectedTargetMemory?.Priority.FormatZeroDecimal()}, P: {SelectedTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); } /*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index dc50608c8..007c6d8a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -442,8 +442,7 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; } - + if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; } Vector2 spriteOrigin = limb.ActiveSprite.Origin; spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; limb.ActiveSprite.Origin = spriteOrigin; @@ -468,7 +467,10 @@ namespace Barotrauma { var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10); - SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); + if (!limbJoint.Params.BreakSound.IsNullOrEmpty() && !limbJoint.Params.BreakSound.Equals("none", StringComparison.OrdinalIgnoreCase)) + { + SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 890be7cb1..a350ef029 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -42,7 +42,7 @@ namespace Barotrauma if (sound != null) { - SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling); + SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 99557594c..770aa20e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -109,6 +109,22 @@ namespace Barotrauma set => grainStrength = Math.Max(0, value); } + /// + /// Can be used to set camera shake from status effects + /// + public float CameraShake + { + get { return Screen.Selected?.Cam?.Shake ?? 0.0f; } + set + { + if (!MathUtils.IsValid(value)) { return; } + if (Screen.Selected?.Cam != null) + { + Screen.Selected.Cam.Shake = value; + } + } + } + private readonly List bloodEmitters = new List(); public IEnumerable BloodEmitters { @@ -637,18 +653,6 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam) { - if (InvisibleTimer > 0.0f) - { - if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) - { - InvisibleTimer = 0.0f; - } - else - { - InvisibleTimer -= deltaTime; - } - } - foreach (GUIMessage message in guiMessages) { bool wasPending = message.Timer < 0.0f; @@ -965,7 +969,24 @@ namespace Barotrauma } if (IsDead) { return; } - + + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode != EnemyHealthBarMode.ShowAll) + { + if (Controlled == null) + { + if (!IsOnPlayerTeam) { return; } + } + else + { + if (!HumanAIController.IsFriendly(Controlled, this) || + (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) + { + return; + } + } + } + if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index f4742ac64..ff196c018 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Tutorials; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -10,7 +9,7 @@ using System.Linq; namespace Barotrauma { - class CharacterHUD + partial class CharacterHUD { const float BossHealthBarDuration = 120.0f; @@ -100,8 +99,9 @@ namespace Barotrauma } } - public static bool ShouldRecreateHudTexts { get; set; } = true; - private static bool heldDownShiftWhenGotHudTexts; + public static bool RecreateHudTexts { get; set; } = true; + private static bool lastHudTextsContextual; + private static float timeHealthWindowClosed; public static bool IsCampaignInterfaceOpen => GameMain.GameSession?.Campaign != null && @@ -114,7 +114,7 @@ namespace Barotrauma return character?.Inventory != null && !character.Removed && !character.IsKnockedDown && - (controller?.User != character || !controller.HideHUD) && + (controller?.User != character || !controller.HideHUD || Screen.Selected.IsEditor) && !IsCampaignInterfaceOpen && !ConversationAction.FadeScreenToBlack; } @@ -175,7 +175,8 @@ namespace Barotrauma if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen) { bool mouseOnPortrait = MouseOnCharacterPortrait() && GUI.MouseOn == null; - if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None()) + bool healthWindowOpen = CharacterHealth.OpenHealthWindow != null || timeHealthWindowClosed < 0.2f; + if (mouseOnPortrait && !healthWindowOpen && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None()) { CharacterHealth.OpenHealthWindow = character.CharacterHealth; } @@ -217,7 +218,7 @@ namespace Barotrauma if (focusedItemOverlayTimer <= 0.0f) { focusedItem = null; - ShouldRecreateHudTexts = true; + RecreateHudTexts = true; } } } @@ -243,6 +244,15 @@ namespace Barotrauma } } } + + if (CharacterHealth.OpenHealthWindow != null) + { + timeHealthWindowClosed = 0.0f; + } + else + { + timeHealthWindowClosed += deltaTime; + } } public static void Draw(SpriteBatch spriteBatch, Character character, Camera cam) @@ -307,7 +317,7 @@ namespace Barotrauma { if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); - if (alpha <= 0.0f) continue; + if (alpha <= 0.0f) { continue; } GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } @@ -330,7 +340,7 @@ namespace Barotrauma if (focusedItem != character.FocusedItem) { focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); - ShouldRecreateHudTexts = true; + RecreateHudTexts = true; } focusedItem = character.FocusedItem; } @@ -354,14 +364,14 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - bool shiftDown = PlayerInput.IsShiftDown(); - if (ShouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + bool hudTextsContextual = PlayerInput.IsShiftDown(); + if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual) { - ShouldRecreateHudTexts = true; - heldDownShiftWhenGotHudTexts = shiftDown; + RecreateHudTexts = true; + lastHudTextsContextual = hudTextsContextual; } - var hudTexts = focusedItem.GetHUDTexts(character, ShouldRecreateHudTexts); - ShouldRecreateHudTexts = false; + var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts); + RecreateHudTexts = false; int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); @@ -492,7 +502,17 @@ namespace Barotrauma { var item = character.Inventory.GetItemAt(i); if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } - + //if the item is also equipped in another slot we already went through, don't draw the hud again + bool duplicateFound = false; + for (int j = 0; j < i; j++) + { + if (character.Inventory.SlotTypes[j] != InvSlotType.Any && character.Inventory.GetItemAt(j) == item) + { + duplicateFound = true; + break; + } + } + if (duplicateFound) { continue; } foreach (ItemComponent ic in item.Components) { if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); } @@ -548,7 +568,7 @@ namespace Barotrauma if (CharacterHealth.OpenHealthWindow == character.SelectedCharacter.CharacterHealth) { character.SelectedCharacter.CharacterHealth.Alignment = Alignment.Left; - character.SelectedCharacter.CharacterHealth.DrawStatusHUD(spriteBatch); + //character.SelectedCharacter.CharacterHealth.DrawStatusHUD(spriteBatch); } } else if (character.Inventory != null) @@ -644,6 +664,12 @@ namespace Barotrauma { if (character == null || character.IsDead || character.Removed) { return; } + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode == EnemyHealthBarMode.HideAll) + { + return; + } + var existingBar = bossHealthBars.Find(b => b.Character == character); if (existingBar != null) { @@ -669,6 +695,8 @@ namespace Barotrauma public static void UpdateBossHealthBars(float deltaTime) { + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + for (int i = 0; i < bossHealthBars.Count; i++) { var bossHealthBar = bossHealthBars[i]; @@ -710,7 +738,7 @@ namespace Barotrauma for (int i = bossHealthBars.Count - 1; i >= 0 ; i--) { var bossHealthBar = bossHealthBars[i]; - if (bossHealthBar.FadeTimer <= 0) + if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll) { bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); @@ -762,5 +790,25 @@ namespace Barotrauma Vector2 drawPos = objectiveEntity.Entity.WorldPosition;// + Vector2.UnitX * objectiveEntity.Sprite.size.X * 1.5f; GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, objectiveEntity.Sprite, objectiveEntity.Color * iconAlpha); } + + static partial void RecreateHudTextsIfControllingProjSpecific(Character character) + { + if (character == Character.Controlled) + { + RecreateHudTexts = true; + } + } + + static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items) + { + foreach (var item in items) + { + if (item == Character.Controlled?.FocusedItem) + { + RecreateHudTexts = true; + break; + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index ab2942b42..b472749ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -78,7 +78,7 @@ namespace Barotrauma Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) { ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero @@ -92,8 +92,8 @@ namespace Barotrauma } if (Job != null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) { Padding = Vector4.Zero }; @@ -101,7 +101,7 @@ namespace Barotrauma if (PersonalityTrait != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), PersonalityTrait.DisplayName), font: font) { @@ -109,7 +109,23 @@ namespace Barotrauma }; } - if (Job != null && (Character == null || !Character.IsDead)) + GUIButton manageTalentButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), + text: TextManager.Get("ClientPermission.ManageBotTalents"), style: "GUIButtonSmall") + { + Enabled = false, + UserData = TalentMenu.ManageBotTalentsButtonUserData, + TextBlock = + { + AutoScaleHorizontal = true + } + }; + + if (TalentMenu.CanManageTalents(this)) + { + manageTalentButton.Enabled = true; + } + + if (Job != null && Character is not { IsDead: true }) { var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { @@ -120,7 +136,7 @@ namespace Barotrauma skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; - + foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); @@ -144,7 +160,7 @@ namespace Barotrauma } } } - else if (Character != null && Character.IsDead) + else if (Character is { IsDead: true }) { var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 4a9a99547..23db24f69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -113,9 +113,9 @@ namespace Barotrauma } } - public void ClientWriteInput(IWriteMessage msg) + public void ClientWriteInput(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { - msg.WriteByte((byte)ClientNetObject.CHARACTER_INPUT); + segmentTableWriter.StartNewSegment(ClientNetSegment.CharacterInput); if (memInput.Count > 60) { @@ -501,7 +501,7 @@ namespace Barotrauma info?.ClearSavedStatValues(statType); for (int i = 0; i < savedStatValueCount; i++) { - string statIdentifier = msg.ReadString(); + Identifier statIdentifier = msg.ReadIdentifier(); float statValue = msg.ReadSingle(); bool removeOnDeath = msg.ReadBoolean(); info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 983906c72..f39de7890 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -32,12 +32,6 @@ namespace Barotrauma public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay; - private readonly static LocalizedString[] strengthTexts = new LocalizedString[] - { - TextManager.Get("AfflictionStrengthLow"), - TextManager.Get("AfflictionStrengthMedium"), - TextManager.Get("AfflictionStrengthHigh") - }; private Point screenResolution; @@ -89,11 +83,23 @@ namespace Barotrauma private int selectedLimbIndex = -1; private LimbHealth currentDisplayedLimb; + /// + /// Container for the icons above the health bar + /// + private GUIComponent afflictionIconContainer; + + private GUIButton showHiddenAfflictionsButton; + + /// + /// Container for passive afflictions that have been hidden from afflictionIconContainer + /// + private GUIComponent hiddenAfflictionIconContainer; + private GUIProgressBar healthWindowHealthBar; private GUIProgressBar healthWindowHealthBarShadow; private GUITextBlock characterName; - private GUIListBox afflictionIconContainer; + private GUIListBox afflictionIconList; private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; @@ -150,7 +156,7 @@ namespace Barotrauma Character.Controlled.DeselectCharacter(); } - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; if (openHealthWindow != null) { if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier())) @@ -331,7 +337,7 @@ namespace Barotrauma deadIndicator.AutoScaleHorizontal = true; } - afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); + afflictionIconList = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), TextManager.Get("SuitableTreatments"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); @@ -379,6 +385,26 @@ namespace Barotrauma Enabled = true }; + afflictionIconContainer = new GUILayoutGroup( + HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + showHiddenAfflictionsButton = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: "GUIButtonCircular") + { + Visible = false, + CanBeFocused = false + }; + + hiddenAfflictionIconContainer = new GUILayoutGroup( + HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + UpdateAlignment(); SuicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) @@ -596,7 +622,7 @@ namespace Barotrauma public void UpdateHUD(float deltaTime) { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } if (openHealthWindow != null) { if (openHealthWindow != Character.Controlled?.CharacterHealth && openHealthWindow != Character.Controlled?.SelectedCharacter?.CharacterHealth) @@ -700,6 +726,8 @@ namespace Barotrauma distortTimer = 0.0f; } + UpdateStatusHUD(deltaTime); + if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null && Character.Controlled.AllowInput && !toggledThisFrame) { @@ -726,9 +754,9 @@ namespace Barotrauma OpenHealthWindow = null; } - foreach (GUIComponent afflictionIcon in afflictionIconContainer.Content.Children) + foreach (GUIComponent afflictionIcon in afflictionIconList.Content.Children) { - if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } + if (afflictionIcon.UserData is not Affliction affliction) { continue; } if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { afflictionIcon.Flash(GUIStyle.Red); @@ -900,7 +928,7 @@ namespace Barotrauma healthBarHolder.CanBeFocused = healthBar.CanBeFocused = healthBarShadow.CanBeFocused = !Character.ShouldLockHud(); if (Character.AllowInput && UseHealthWindow && !Character.DisableHealthWindow && healthBar.Enabled && healthBar.CanBeFocused && - (GUI.IsMouseOn(healthBar) || highlightedAfflictionIcon != null) && Inventory.SelectedSlot == null) + (GUI.IsMouseOn(healthBar) || GUI.MouseOn?.UserData is AfflictionPrefab) && Inventory.SelectedSlot == null) { healthBar.State = GUIComponent.ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -960,7 +988,12 @@ namespace Barotrauma } else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) { - healthBarHolder.AddToGUIUpdateList(); + healthBarHolder.AddToGUIUpdateList(); + afflictionIconContainer.AddToGUIUpdateList(); + if (hiddenAfflictionIconContainer.Visible) + { + hiddenAfflictionIconContainer.AddToGUIUpdateList(); + } } if (SuicideButton.Visible && Character == Character.Controlled) { @@ -989,7 +1022,7 @@ namespace Barotrauma if (affliction.Prefab.AfflictionOverlay != null) { Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; - ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * (affliction.GetAfflictionOverlayMultiplier()), Vector2.Zero, 0.0f, + ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); } } @@ -1021,94 +1054,134 @@ namespace Barotrauma // If manning a turret the portrait doesn't get rendered so we push the health bar to remove the empty gap healthBarHolder.RectTransform.ScreenSpaceOffset = Character.ShouldLockHud() ? new Point(0, HUDLayoutSettings.PortraitArea.Height) : Point.Zero; } - - DrawStatusHUD(spriteBatch); } - private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; - public void DrawStatusHUD(SpriteBatch spriteBatch) + //private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; + + private readonly List statusIcons = new List(); + private readonly Dictionary statusIconVisibleTime = new Dictionary(); + private const float HideStatusIconDelay = 5.0f; + + public void UpdateStatusHUD(float deltaTime) { - highlightedAfflictionIcon = null; - //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - var statusIcons = new List<(Affliction Affliction, LocalizedString Warning)>(); + statusIcons.Clear(); if (Character.InPressure) { - statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); + statusIcons.Add(pressureAffliction); } if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) { - statusIcons.Add((oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + statusIcons.Add(oxygenLowAffliction); } foreach (Affliction affliction in currentDisplayedAfflictions) { - statusIcons.Add((affliction, affliction.Prefab.Name)); + statusIcons.Add(affliction); } - Vector2 highlightedIconPos = Vector2.Zero; - Rectangle afflictionArea = HUDLayoutSettings.AfflictionAreaLeft; - - // Push the icons down since the portrait doesn't get rendered + int spacing = GUI.IntScale(10); if (Character.ShouldLockHud()) { - afflictionArea.Y += HUDLayoutSettings.PortraitArea.Height; + // Push the icons down since the portrait doesn't get rendered + afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, HUDLayoutSettings.PortraitArea.Height); + hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing + HUDLayoutSettings.PortraitArea.Height); } + else + { + afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, 0); + hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing); + } + //remove affliction icons for afflictions that no longer exist - bool horizontal = afflictionArea.Width > afflictionArea.Height; - int iconSize = horizontal ? afflictionArea.Height : afflictionArea.Width; - - Point pos = new Point(afflictionArea.Right - iconSize, afflictionArea.Top); + RemoveNonExistentIcons(afflictionIconContainer); + RemoveNonExistentIcons(hiddenAfflictionIconContainer); + void RemoveNonExistentIcons(GUIComponent container) + { + for (int i = container.CountChildren - 1; i >= 0; i--) + { + var child = container.GetChild(i); + if (child.UserData is not AfflictionPrefab afflictionPrefab) { continue; } + if (!statusIcons.Any(s => s.Prefab == afflictionPrefab)) + { + container.RemoveChild(child); + statusIconVisibleTime.Remove(afflictionPrefab); + } + } + } foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.Affliction; + Affliction affliction = statusIcon; AfflictionPrefab afflictionPrefab = affliction.Prefab; - Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); - if (afflictionIconRect.Contains(PlayerInput.MousePosition) && !Character.ShouldLockHud() && GUI.MouseOn == null) + if (!statusIconVisibleTime.ContainsKey(afflictionPrefab)) { statusIconVisibleTime.Add(afflictionPrefab, 0.0f); } + statusIconVisibleTime[afflictionPrefab] += deltaTime; + + var matchingIcon = + afflictionIconContainer.GetChildByUserData(afflictionPrefab) ?? + hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab); + if (matchingIcon == null) { - highlightedAfflictionIcon = statusIcon; - highlightedIconPos = afflictionIconRect.Location.ToVector2(); + matchingIcon = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: null) + { + UserData = afflictionPrefab, + ToolTip = affliction.Prefab.Name, + CanBeSelected = false + }; + if (affliction == pressureAffliction) + { + matchingIcon.ToolTip = TextManager.Get("PressureHUDWarning"); + } + else if (affliction == pressureAffliction) + { + matchingIcon.ToolTip = TextManager.Get("OxygenHUDWarning"); + } + new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true) + { + CanBeFocused = false + }; } - - if (affliction.DamagePerSecond > 1.0f) + if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay) { - Rectangle glowRect = afflictionIconRect; - glowRect.Inflate((int)(20 * GUI.Scale), (int)(20 * GUI.Scale)); - var glow = GUIStyle.GetComponentStyle("OuterGlowCircular"); - glow.Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, glowRect, - GUIStyle.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); + matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform; } + var image = matchingIcon.GetChild(); + image.Color = GetAfflictionIconColor(afflictionPrefab, affliction); + image.HoverColor = Color.Lerp(image.Color, Color.White, 0.5f); - float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; - - afflictionPrefab.Icon?.Draw(spriteBatch, - pos.ToVector2(), - /*highlightedIcon == statusIcon ? statusIcon.First.Prefab.IconColor : statusIcon.First.Prefab.IconColor * 0.8f,*/ // OLD IMPLEMENTATION - GetAfflictionIconColor(afflictionPrefab, affliction) * alphaMultiplier, - rotate: 0, - scale: iconSize / afflictionPrefab.Icon.size.X); - - if (horizontal) - pos.X -= iconSize + (int)(5 * GUI.Scale); - else - pos.Y += iconSize + (int)(5 * GUI.Scale); + if (affliction.DamagePerSecond > 1.0f && matchingIcon.FlashTimer <= 0.0f) + { + matchingIcon.Flash(useCircularFlash: true, flashDuration: 1.5f, flashRectInflate: Vector2.One * 15.0f * GUI.Scale); + image.Pulsate(Vector2.One, Vector2.One * 1.2f, 1.0f); + } } - if (highlightedAfflictionIcon != null) + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => { - LocalizedString nameTooltip = highlightedAfflictionIcon.Value.NameToolTip; - Vector2 offset = GUIStyle.Font.MeasureString(nameTooltip); + if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } + if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } + var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); + var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); + return index1.CompareTo(index2); + }); + (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; - GUI.DrawString(spriteBatch, - alignment == Alignment.Left ? highlightedIconPos + offset : highlightedIconPos - offset, - nameTooltip, - Color.White * 0.8f, Color.Black * 0.5f); + Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; + foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) + { + hiddenAfflictionHoverArea = Rectangle.Union(hiddenAfflictionHoverArea, child.Rect); } + afflictionIconContainer.Visible = true; + hiddenAfflictionIconContainer.Visible = + showHiddenAfflictionsButton.Rect.Contains(PlayerInput.MousePosition) || + (hiddenAfflictionIconContainer.Visible && hiddenAfflictionHoverArea.Contains(PlayerInput.MousePosition)); + showHiddenAfflictionsButton.Visible = hiddenAfflictionIconContainer.CountChildren > 0; + showHiddenAfflictionsButton.IgnoreLayoutGroups = !showHiddenAfflictionsButton.Visible; + showHiddenAfflictionsButton.Text = $"+{hiddenAfflictionIconContainer.CountChildren}"; + if (Vitality > 0.0f) { float currHealth = healthBar.BarSize; @@ -1126,6 +1199,7 @@ namespace Barotrauma } else { + afflictionIconContainer.Visible = hiddenAfflictionIconContainer.Visible = false; if (Vitality > 0.0f) { float currHealth = healthWindowHealthBar.BarSize; @@ -1150,18 +1224,20 @@ namespace Barotrauma public static Color GetAfflictionIconColor(AfflictionPrefab prefab, float afflictionStrength) { + //use sqrt to make the color change rapidly when strength is low + //(low strength is where seeing the severity of the affliction makes more difference - at high strengths the character is already unconscious or dead) + float colorT = MathF.Sqrt(afflictionStrength / prefab.MaxStrength); // No specific colors, use generic if (prefab.IconColors == null) { if (prefab.IsBuff) { - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); + return ToolBox.GradientLerp(colorT, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); } - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); + return ToolBox.GradientLerp(colorT, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); } - - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); + return ToolBox.GradientLerp(colorT, prefab.IconColors); } public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); @@ -1172,7 +1248,7 @@ namespace Barotrauma { if (selectedLimb == null) { - afflictionIconContainer.Content.ClearChildren(); + afflictionIconList.Content.ClearChildren(); return; } @@ -1207,7 +1283,7 @@ namespace Barotrauma private void CreateAfflictionInfos(IEnumerable afflictions) { - afflictionIconContainer.ClearChildren(); + afflictionIconList.ClearChildren(); displayedAfflictions.Clear(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions, excludeBuffs: false).FirstOrDefault(); @@ -1217,7 +1293,7 @@ namespace Barotrauma { displayedAfflictions.Add((affliction, affliction.Strength)); - var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform), style: "ListBoxElement") + var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconList.Content.RectTransform), style: "ListBoxElement") { UserData = affliction, OnClicked = SelectAffliction @@ -1275,7 +1351,7 @@ namespace Barotrauma } buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData); - afflictionIconContainer.RecalculateChildren(); + afflictionIconList.RecalculateChildren(); } private void CreateRecommendedTreatments() @@ -1386,7 +1462,7 @@ namespace Barotrauma recommendedTreatmentContainer.RecalculateChildren(); - afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) => + afflictionIconList.Content.RectTransform.SortChildren((r1, r2) => { var first = r1.GUIComponent.UserData as Affliction; var second = r2.GUIComponent.UserData as Affliction; @@ -1437,7 +1513,10 @@ namespace Barotrauma }; var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), - affliction.Prefab.Description, textAlignment: Alignment.TopLeft, wrap: true) + affliction.Prefab.GetDescription( + affliction.Strength, + Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter), + textAlignment: Alignment.TopLeft, wrap: true) { CanBeFocused = false }; @@ -1449,8 +1528,7 @@ namespace Barotrauma Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f)); - afflictionStrength.Text = strengthTexts[ - MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + afflictionStrength.Text = affliction.GetStrengthText(); Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text); @@ -1482,10 +1560,9 @@ namespace Barotrauma private bool SelectAffliction(GUIButton button, object userData) { bool selected = button.Selected; - foreach (var child in afflictionIconContainer.Content.Children) + foreach (var child in afflictionIconList.Content.Children) { - GUIButton btn = child.GetChild(); - if (btn != null) + if (child is GUIButton btn) { btn.Selected = btn == button && !selected; } @@ -1516,7 +1593,7 @@ namespace Barotrauma afflictionEffectColor = GUIStyle.Green; } - var child = afflictionIconContainer.Content.FindChild(affliction); + var child = afflictionIconList.Content.FindChild(affliction); var afflictionStrengthPredictionBar = child.GetChild().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar; afflictionStrengthPredictionBar.BarSize = 0.0f; @@ -1542,6 +1619,11 @@ namespace Barotrauma } } + if (!affliction.Prefab.ShowBarInHealthMenu) + { + afflictionStrengthBar.BarSize = 1f; + } + if (afflictionTooltip != null && afflictionTooltip.UserData == affliction) { UpdateAfflictionInfo(afflictionTooltip.Content, affliction); @@ -1581,8 +1663,7 @@ namespace Barotrauma var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock; - strengthText.Text = strengthTexts[ - MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + strengthText.Text = affliction.GetStrengthText(); strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); @@ -1836,14 +1917,14 @@ namespace Barotrauma i++; } - if (selectedLimbIndex > -1 && afflictionIconContainer.Content.CountChildren > 0) + if (selectedLimbIndex > -1 && afflictionIconList.Content.CountChildren > 0) { LimbHealth limbHealth = limbHealths[selectedLimbIndex]; if (limbHealth?.IndicatorSprite != null) { Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawLine(spriteBatch, - new Vector2(afflictionIconContainer.Rect.X, afflictionIconContainer.Rect.Y), + new Vector2(afflictionIconList.Rect.X, afflictionIconList.Rect.Y), selectedLimbArea.Center.ToVector2(), Color.LightGray * 0.5f, width: 4); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index e59354ecb..92ce38770 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -51,11 +51,10 @@ namespace Barotrauma && p.InstallTime.TryUnwrap(out var installTime) && item.LatestUpdateTime <= installTime)) .ToArray(); - if (needInstalling.Any()) - { - await Task.WhenAll( - needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); - } + if (!needInstalling.Any()) { return Enumerable.Empty(); } + + await Task.WhenAll( + needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); return needInstalling; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 09d491bfb..da5698ef6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -30,7 +30,7 @@ namespace Barotrauma public void ClientExecute(string[] args) { - bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); + bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true }); if (!allowCheats && !CheatsEnabled && IsCheat) { NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); @@ -128,21 +128,17 @@ namespace Barotrauma public static void Update(float deltaTime) { - lock (queuedMessages) + while (queuedMessages.TryDequeue(out var newMsg)) { - while (queuedMessages.Count > 0) - { - var newMsg = queuedMessages.Dequeue(); - AddMessage(newMsg); + AddMessage(newMsg); - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + { + unsavedMessages.Add(newMsg); + if (unsavedMessages.Count >= messagesPerFile) { - unsavedMessages.Add(newMsg); - if (unsavedMessages.Count >= messagesPerFile) - { - SaveLogs(); - unsavedMessages.Clear(); - } + SaveLogs(); + unsavedMessages.Clear(); } } } @@ -178,9 +174,9 @@ namespace Barotrauma Character.DisableControls = true; - if (PlayerInput.KeyHit(Keys.Tab)) + if (PlayerInput.KeyHit(Keys.Tab) && !textBox.IsIMEActive) { - textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 ); + textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 ); } if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) @@ -260,25 +256,21 @@ namespace Barotrauma public static void DequeueMessages() { - lock (queuedMessages) + while (queuedMessages.TryDequeue(out var newMsg)) { - while (queuedMessages.Count > 0) + if (listBox == null) { - var newMsg = queuedMessages.Dequeue(); - if (listBox == null) - { - //don't attempt to add to the listbox if it hasn't been created yet - Messages.Add(newMsg); - } - else - { - AddMessage(newMsg); - } + //don't attempt to add to the listbox if it hasn't been created yet + Messages.Add(newMsg); + } + else + { + AddMessage(newMsg); + } - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) - { - unsavedMessages.Add(newMsg); - } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + { + unsavedMessages.Add(newMsg); } } } @@ -1138,6 +1130,17 @@ namespace Barotrauma }); AssignRelayToServer("debugdraw", false); + AssignOnExecute("debugdrawlocalization", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !TextManager.DebugDraw; + } + TextManager.DebugDraw = state; + NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White); + }); + AssignRelayToServer("debugdraw", false); + AssignOnExecute("togglevoicechatfilters", (string[] args) => { if (args.None() || !bool.TryParse(args[0], out bool state)) @@ -1697,6 +1700,8 @@ namespace Barotrauma config.Language = language; GameSettings.SetCurrentConfig(config); } + + HashSet missingTexts = new HashSet(); //key = text tag, value = list of languages the tag is missing from Dictionary> missingTags = new Dictionary>(); @@ -1757,20 +1762,38 @@ namespace Barotrauma foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) { - foreach (var property in itemComponentType.GetProperties()) + checkSerializableEntityType(itemComponentType); + } + checkSerializableEntityType(typeof(Item)); + checkSerializableEntityType(typeof(Hull)); + checkSerializableEntityType(typeof(Structure)); + + void checkSerializableEntityType(Type t) + { + foreach (var property in t.GetProperties()) { - if (!property.IsDefined(typeof(InGameEditable), false)) { continue; } + if (!property.IsDefined(typeof(Editable), false)) { continue; } string propertyTag = $"{property.DeclaringType.Name}.{property.Name}"; - addIfMissingAll(language, + if (addIfMissingAll(language, propertyTag.ToIdentifier(), property.Name.ToIdentifier(), - $"sp.{propertyTag}.name".ToIdentifier()); + $"sp.{property.Name}.name".ToIdentifier(), + $"sp.{propertyTag}.name".ToIdentifier()) && language == "English".ToLanguageIdentifier()) + { + missingTexts.Add($"{property.Name.FormatCamelCaseWithSpaces()}"); + } - addIfMissingAll(language, + var description = (property.GetCustomAttributes(true).First(a => a is Serialize) as Serialize).Description; + + if (addIfMissingAll(language, $"sp.{propertyTag}.description".ToIdentifier(), - $"{property.Name.ToIdentifier()}.description".ToIdentifier()); + $"sp.{property.Name}.description".ToIdentifier(), + $"{property.Name.ToIdentifier()}.description".ToIdentifier()) && language == "English".ToLanguageIdentifier()) + { + missingTexts.Add($"{description}"); + } } } @@ -1794,7 +1817,18 @@ namespace Barotrauma Identifier afflictionId = affliction.TranslationIdentifier; addIfMissing($"afflictionname.{afflictionId}".ToIdentifier(), language); - addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); + + if (affliction.Descriptions.Any()) + { + foreach (var description in affliction.Descriptions) + { + addIfMissing(description.TextTag, language); + } + } + else + { + addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); + } } foreach (var talentTree in TalentTree.JobTalentTrees) @@ -1891,6 +1925,23 @@ namespace Barotrauma ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); SwapLanguage(TextManager.DefaultLanguage); + if (missingTexts.Any()) + { + ShowQuestionPrompt("Dump the property names and descriptions missing from English to a new xml file? Y/N", + (option) => + { + if (option.ToLowerInvariant() == "y") + { + string path = "newtexts.txt"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; + File.WriteAllLines(path, missingTexts); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + ToolBox.OpenFileWithShell(Path.GetFullPath(path)); + SwapLanguage(TextManager.DefaultLanguage); + } + }); + } + void addIfMissing(Identifier tag, LanguageIdentifier language) { if (!tags[language].Contains(tag)) @@ -1899,15 +1950,90 @@ namespace Barotrauma missingTags[tag].Add(language); } } - void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) + bool addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) { if (!potentialTags.Any(t => tags[language].Contains(t))) { var tag = potentialTags.First(); if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet(); } missingTags[tag].Add(language); + return true; + } + return false; + } + })); + + + commands.Add(new Command("checkduplicateloca", "", (string[] args) => + { + if (args.Length < 1) + { + ThrowError("Please specify a file path."); + return; + } + XDocument doc1 = XMLExtensions.TryLoadXml(args[0]); + if (doc1?.Root == null) + { + ThrowError($"Could not load the file \"{args[0]}\""); + return; + } + List<(string tag, string text)> texts = new List<(string tag, string text)>(); + + bool duplicatesFound = false; + foreach (XElement element in doc1.Root.Elements()) + { + string tag = element.Name.ToString(); + string text = element.ElementInnerText(); + if (texts.Any(t => t.tag == tag)) + { + ThrowError($"Duplicate tag \"{tag}\"."); + duplicatesFound = true; } } + if (duplicatesFound) + { + ThrowError($"Aborting, please fix duplicate tags in the file and try again."); + return; + } + + foreach (XElement element in doc1.Root.Elements()) + { + string tag = element.Name.ToString(); + string text = element.ElementInnerText(); + if (texts.Any(t => t.text == text)) + { + if (tag.StartsWith("sp.")) + { + string[] split = tag.Split('.'); + if (split.Length > 3) + { + texts.RemoveAll(t => t.text == text); + string newTag = $"sp.{split[2]}.{split[3]}"; + texts.Add((newTag, text)); + NewMessage($"Duplicate text \"{tag}\", merging to \"{newTag}\"."); + } + else + { + NewMessage($"Duplicate text \"{tag}\", using existing one \"{texts.Find(t => t.text == text).tag}\"."); + } + } + else + { + texts.Add((tag, text)); + ThrowError($"Duplicate text \"{tag}\". Could not determine if the text can be merged with an existing one, please check it manually."); + } + } + else + { + texts.Add((tag, text)); + } + } + + string filePath = "uniquetexts.xml"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; + File.WriteAllLines(filePath, texts.Select(t => $"<{t.tag}>{t.text}")); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => @@ -2087,6 +2213,15 @@ namespace Barotrauma } })); + commands.Add(new Command("spawnallitems", "", (string[] args) => + { + var cursorPos = Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? Vector2.Zero; + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, cursorPos); + } + })); + commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) => { float defaultZoom = Screen.Selected.Cam.DefaultZoom; @@ -2587,99 +2722,6 @@ namespace Barotrauma })); #endif - commands.Add(new Command("cleanbuild", "", (string[] args) => - { - /*GameSettings.CurrentConfig.MusicVolume = 0.5f; - GameSettings.CurrentConfig.SoundVolume = 0.5f; - GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true; - GameSettings.CurrentConfig.VoipAttenuationEnabled = true; - NewMessage("Music and sound volume set to 0.5", Color.Green); - - GameSettings.CurrentConfig.GraphicsWidth = 0; - GameSettings.CurrentConfig.GraphicsHeight = 0; - GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed; - NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green); - NewMessage("Fullscreen enabled", Color.Green); - - GameSettings.CurrentConfig.VerboseLogging = false; - - if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster") - { - ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!"); - } - - GameSettings.SaveCurrentConfig();*/ - throw new NotImplementedException(); - #warning TODO: reimplement - - var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder); - - foreach (string saveFile in saveFiles) - { - Barotrauma.IO.File.Delete(saveFile); - NewMessage("Deleted " + saveFile, Color.Green); - } - - if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"))) - { - Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true); - NewMessage("Deleted temp save folder", Color.Green); - } - - if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath)) - { - var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath); - - foreach (string logFile in logFiles) - { - Barotrauma.IO.File.Delete(logFile); - NewMessage("Deleted " + logFile, Color.Green); - } - } - - if (Barotrauma.IO.File.Exists("filelist.xml")) - { - Barotrauma.IO.File.Delete("filelist.xml"); - NewMessage("Deleted filelist", Color.Green); - } - - if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt")) - { - Barotrauma.IO.File.Delete("Data/bannedplayers.txt"); - NewMessage("Deleted bannedplayers.txt", Color.Green); - } - - if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub")) - { - Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub"); - - NewMessage("Deleted TutorialSub from the submarine folder", Color.Green); - } - - /*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile)) - { - Barotrauma.IO.File.Delete(GameServer.SettingsFile); - NewMessage("Deleted server settings", Color.Green); - } - - if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile)) - { - Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile); - NewMessage("Deleted client permission file", Color.Green); - }*/ - - if (Barotrauma.IO.File.Exists("crashreport.log")) - { - Barotrauma.IO.File.Delete("crashreport.log"); - NewMessage("Deleted crashreport.log", Color.Green); - } - - if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub")) - { - ThrowError("TutorialSub.sub not found!"); - } - })); - commands.Add(new Command("reloadcorepackage", "", (string[] args) => { if (args.Length < 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs b/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs index 474f8c570..be47a0793 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs @@ -1,75 +1,22 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; -using System; -using System.Runtime.InteropServices; -using System.Text; namespace EventInput { - public class CharacterEventArgs : EventArgs + public readonly record struct CharacterEventArgs(char Character, long Param) { - private readonly char character; - private readonly long lParam; - - public CharacterEventArgs(char character, long lParam) - { - this.character = character; - this.lParam = lParam; - } - - public char Character - { - get { return character; } - } - - public long Param - { - get { return lParam; } - } - - public long RepeatCount - { - get { return lParam & 0xffff; } - } - - public bool ExtendedKey - { - get { return (lParam & (1 << 24)) > 0; } - } - - public bool AltPressed - { - get { return (lParam & (1 << 29)) > 0; } - } - - public bool PreviousState - { - get { return (lParam & (1 << 30)) > 0; } - } - - public bool TransitionState - { - get { return (lParam & (1 << 31)) > 0; } - } + public long RepeatCount => Param & 0xffff; + public bool ExtendedKey => (Param & (1 << 24)) > 0; + public bool AltPressed => (Param & (1 << 29)) > 0; + public bool PreviousState => (Param & (1 << 30)) > 0; + public bool TransitionState => (Param & (1 << 31)) > 0; } - public class KeyEventArgs : EventArgs - { - private Keys keyCode; - - public KeyEventArgs(Keys keyCode) - { - this.keyCode = keyCode; - } - - public Keys KeyCode - { - get { return keyCode; } - } - } + public readonly record struct KeyEventArgs(Keys KeyCode, char Character); public delegate void CharEnteredHandler(object sender, CharacterEventArgs e); public delegate void KeyEventHandler(object sender, KeyEventArgs e); + public delegate void EditingTextHandler(object sender, TextEditingEventArgs e); public static class EventInput { @@ -88,6 +35,12 @@ namespace EventInput /// public static event KeyEventHandler KeyUp; + /// + /// Raised when the user is editing text and IME is in progress. + /// + public static event EditingTextHandler EditingText; + + static bool initialized; /// @@ -100,8 +53,10 @@ namespace EventInput { return; } - + window.TextInput += ReceiveInput; + window.KeyDown += ReceiveKeyDown; + window.TextEditing += ReceiveTextEditing; initialized = true; } @@ -109,7 +64,16 @@ namespace EventInput private static void ReceiveInput(object sender, TextInputEventArgs e) { OnCharEntered(e.Character); - KeyDown?.Invoke(sender, new KeyEventArgs(e.Key)); + } + + private static void ReceiveKeyDown(object sender, TextInputEventArgs e) + { + KeyDown?.Invoke(sender, new KeyEventArgs(e.Key, e.Character)); + } + + private static void ReceiveTextEditing(object sender, TextEditingEventArgs e) + { + EditingText?.Invoke(sender, e); } public static void OnCharEntered(char character) diff --git a/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs b/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs index b52e392a1..dc25c25d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs @@ -1,8 +1,4 @@ -using System; -using System.Threading; -#if WINDOWS -using System.Windows; -#endif +using Barotrauma; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -14,6 +10,7 @@ namespace EventInput void ReceiveTextInput(string text); void ReceiveCommandInput(char command); void ReceiveSpecialInput(Keys key); + void ReceiveEditingInput(string text, int start, int length); bool Selected { get; set; } //or Focused } @@ -25,47 +22,27 @@ namespace EventInput EventInput.Initialize(window); EventInput.CharEntered += EventInput_CharEntered; EventInput.KeyDown += EventInput_KeyDown; + EventInput.EditingText += EventInput_TextEditing; + GameMain.ResetIMEWorkaround(); + } + + public void EventInput_TextEditing(object sender, TextEditingEventArgs e) + { + _subscriber?.ReceiveEditingInput(e.Text, e.Start, e.Length); } public void EventInput_KeyDown(object sender, KeyEventArgs e) { - if (_subscriber == null) - return; - - _subscriber.ReceiveSpecialInput(e.KeyCode); + _subscriber?.ReceiveSpecialInput(e.KeyCode); + if (char.IsControl(e.Character)) + { + _subscriber?.ReceiveCommandInput(e.Character); + } } void EventInput_CharEntered(object sender, CharacterEventArgs e) { - if (_subscriber == null) - return; - if (char.IsControl(e.Character)) - { - _subscriber.ReceiveCommandInput(e.Character); - // Doesn't work as expected. Not sure why this should be run in a separate thread. - //#if WINDOWS - // //ctrl-v - // if (e.Character == 0x16) // 22 - // { - // //XNA runs in Multiple Thread Apartment state, which cannot recieve clipboard - // Thread thread = new Thread(PasteThread); - // thread.SetApartmentState(ApartmentState.STA); - // thread.Start(); - // thread.Join(); - // _subscriber.ReceiveTextInput(_pasteResult); - // } - // else - // { - // _subscriber.ReceiveCommandInput(e.Character); - // } - //#else - // _subscriber.ReceiveCommandInput(e.Character); - //#endif - } - else - { - _subscriber.ReceiveTextInput(e.Character); - } + _subscriber?.ReceiveTextInput(e.Character); } IKeyboardSubscriber _subscriber; @@ -74,12 +51,26 @@ namespace EventInput get { return _subscriber; } set { - if (_subscriber == value) return; - if (_subscriber != null) + if (_subscriber == value) { return; } + + if (_subscriber is GUITextBox) + { + TextInput.StopTextInput(); _subscriber.Selected = false; + } + + if (value is GUITextBox box) + { + TextInput.SetTextInputRect(box.MouseRect); + TextInput.StartTextInput(); + TextInput.SetTextInputRect(box.MouseRect); + } + _subscriber = value; if (value != null) + { value.Selected = true; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs new file mode 100644 index 000000000..ce251c05a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -0,0 +1,37 @@ +using Barotrauma.Tutorials; +using Segment = Barotrauma.ObjectiveManager.Segment; + +namespace Barotrauma; + +partial class CheckObjectiveAction : BinaryOptionAction +{ + public enum CheckType + { + Added, + Completed + } + + [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] + public CheckType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + partial void DetermineSuccessProjSpecific(ref bool success) + { + success = false; + if (Identifier.IsEmpty) + { + success = ObjectiveManager.AllActiveObjectivesCompleted(); + } + else if (ObjectiveManager.GetObjective(Identifier) is Segment segment) + { + success = Type switch + { + CheckType.Added => true, + CheckType.Completed => segment.IsCompleted, + _ => false + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 865634dd1..18e93bfbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -323,7 +323,10 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = TextManager.ParseInputTypes(TextManager.Get(text)).Fallback(text); + LocalizedString translatedText = speaker?.DisplayName is not null ? + TextManager.GetWithVariable(text, "[speakername]", speaker?.DisplayName) : + TextManager.Get(text); + translatedText = TextManager.ParseInputTypes(translatedText).Fallback(text); if (speaker?.Info != null && drawChathead) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index b75a1af4a..2cf27ab55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -11,11 +11,13 @@ partial class MessageBoxAction : EventAction if (Type == ActionType.Create || Type == ActionType.ConnectObjective) { CreateMessageBox(); - if (!ObjectiveTag.IsEmpty && GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + if (!ObjectiveTag.IsEmpty) { Identifier id = Identifier.IfEmpty(Text); - var segment = Tutorial.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); - tutorialMode.Tutorial?.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); + var segment = ObjectiveManager.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); + segment.CanBeCompleted = ObjectiveCanBeCompleted; + segment.ParentId = ParentObjectiveId; + ObjectiveManager.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); } } else if (Type == ActionType.Close) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs index ab9b097d3..6420009e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs @@ -1,50 +1,43 @@ -using Barotrauma.Tutorials; - namespace Barotrauma; partial class TutorialSegmentAction : EventAction { - private Tutorial.Segment segment; + private ObjectiveManager.Segment segment; partial void UpdateProjSpecific() { // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) if (Type == SegmentActionType.Trigger) { - segment = Tutorial.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, - new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center), - new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height)); + segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new ObjectiveManager.Segment.Text(TextTag, Width, Height, Anchor.Center), + new ObjectiveManager.Segment.Video(VideoFile, TextTag, Width, Height)); } else if (Type == SegmentActionType.Add) { - segment = Tutorial.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); + segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); } - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + if (segment is not null) { - if (tutorialMode.Tutorial is Tutorial tutorial) - { - switch (Type) - { - case SegmentActionType.Trigger: - case SegmentActionType.Add: - tutorial.TriggerTutorialSegment(segment); - break; - case SegmentActionType.Complete: - tutorial.CompleteTutorialSegment(Identifier); - break; - case SegmentActionType.Remove: - tutorial.RemoveTutorialSegment(Identifier); - break; - case SegmentActionType.CompleteAndRemove: - tutorial.CompleteTutorialSegment(Identifier); - tutorial.RemoveTutorialSegment(Identifier); - break; - } - } + segment.CanBeCompleted = CanBeCompleted; + segment.ParentId = ParentObjectiveId; } - else + switch (Type) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!"); + case SegmentActionType.Trigger: + case SegmentActionType.Add: + ObjectiveManager.TriggerTutorialSegment(segment); + break; + case SegmentActionType.Complete: + ObjectiveManager.CompleteTutorialSegment(Identifier); + break; + case SegmentActionType.Remove: + ObjectiveManager.RemoveTutorialSegment(Identifier); + break; + case SegmentActionType.CompleteAndRemove: + ObjectiveManager.CompleteTutorialSegment(Identifier); + ObjectiveManager.RemoveTutorialSegment(Identifier); + break; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs index 2826454ca..809bdb393 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/UIHighlightAction.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Linq; namespace Barotrauma; @@ -10,37 +11,66 @@ partial class UIHighlightAction : EventAction partial void UpdateProjSpecific() { bool useCircularFlash = false; - GUIComponent component = null; - if (Id != ElementId.None) { - component = GUI.GetAdditions().FirstOrDefault(c => Equals(Id, c.UserData)); + FindAndFlashComponents(c => Equals(Id, c.UserData)); } else if (!EntityIdentifier.IsEmpty) { - component = GUI.GetAdditions().FirstOrDefault(c => + FindAndFlashComponents(c => c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier); } else if (!OrderIdentifier.IsEmpty) { useCircularFlash = true; + bool foundMinimapNode = false; if (!OrderTargetTag.IsEmpty) { - component = - GUI.GetAdditions().FirstOrDefault(c => - c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && - order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); + foundMinimapNode = FindAndFlashComponents(c => + c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && + order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); + } + if (!foundMinimapNode) + { + FindAndFlashComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption, + c => c.UserData is Order order && order.Identifier == OrderIdentifier, + c => Equals(OrderCategory, c.UserData)); } - component ??= - GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption) ?? - GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier) ?? - GUI.GetAdditions().FirstOrDefault(c => Equals(OrderCategory, c.UserData)); } - if (component != null && component.FlashTimer <= 0.0f) + bool FindAndFlashComponents(params Func[] predicates) { - component.Flash(highlightColor, useCircularFlash: useCircularFlash); - component.Bounce |= Bounce; + foreach (var predicate in predicates) + { + if (HighlightMultiple) + { + bool found = false; + foreach (var component in GUI.GetAdditions()) + { + if (predicate(component)) + { + Flash(component); + found = true; + } + }; + return found; + } + else if (GUI.GetAdditions().FirstOrDefault(predicate) is GUIComponent component) + { + Flash(component); + return true; + } + } + return false; + } + + void Flash(GUIComponent component) + { + if (component.FlashTimer <= 0.0f) + { + component.Flash(highlightColor, useCircularFlash: useCircularFlash); + component.Bounce |= Bounce; + } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index b7b05c7f0..1aa23640c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; +using System.Xml.Linq; using Barotrauma.Threading; namespace Barotrauma @@ -37,7 +38,7 @@ namespace Barotrauma private set; } - public bool IsCJK + public TextManager.SpeciallyHandledCharCategory SpeciallyHandledCharCategory { get; private set; @@ -84,17 +85,35 @@ namespace Barotrauma } } + public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element) + => TextManager.SpeciallyHandledCharCategories + .Where(category => element.GetAttributeBool($"is{category}", category switch { + // CJK isn't supported by default + TextManager.SpeciallyHandledCharCategory.CJK => false, + + // For backwards compatibility, we assume that Cyrillic is supported by default + TextManager.SpeciallyHandledCharCategory.Cyrillic => true, + + _ => throw new Exception("unreachable") + })) + .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category); + public ScalableFont(ContentXElement element, GraphicsDevice gd = null) : this( element.GetAttributeContentPath("file")?.Value, (uint)element.GetAttributeInt("size", 14), gd, element.GetAttributeBool("dynamicloading", false), - element.GetAttributeBool("iscjk", false)) + ExtractShccFromXElement(element)) { } - public ScalableFont(string filename, uint size, GraphicsDevice gd = null, bool dynamicLoading = false, bool isCJK = false) + public ScalableFont( + string filename, + uint size, + GraphicsDevice gd = null, + bool dynamicLoading = false, + TextManager.SpeciallyHandledCharCategory speciallyHandledCharCategory = TextManager.SpeciallyHandledCharCategory.None) { lock (globalMutex) { @@ -120,7 +139,7 @@ namespace Barotrauma this.textures = new List(); this.texCoords = new Dictionary(); this.DynamicLoading = dynamicLoading; - this.IsCJK = isCJK; + this.SpeciallyHandledCharCategory = speciallyHandledCharCategory; this.graphicsDevice = gd; if (gd != null && !dynamicLoading) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 2cc5886c8..c1af331a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -775,7 +775,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; - GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()])); + GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()], radio)); if (setText) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 4fcd29809..373fecbbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -357,6 +357,17 @@ namespace Barotrauma string txt = directory; if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } if (!txt.EndsWith("/")) { txt += "/"; } + //get directory info + DirectoryInfo dirInfo = new DirectoryInfo(directory); + try + { + //this will throw an exception if the directory can't be opened + Directory.GetDirectories(directory); + } + catch (UnauthorizedAccessException) + { + continue; + } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { UserData = ItemIsDirectory.Yes diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index c158fc21d..9a84a1b59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; using Barotrauma.CharacterEditor; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -50,6 +49,14 @@ namespace Barotrauma static class GUI { + // Controls where a line is drawn for given coords. + public enum OutlinePosition + { + Default = 0, // Thickness is inside of top left and outside of bottom right coord + Inside = 1, // Thickness is subtracted from the inside + Centered = 2, // Thickness is centered on given coords + Outside = 3, // Tickness is added to the outside + } public static GUICanvas Canvas => GUICanvas.Instance; public static CursorState MouseCursor = CursorState.Default; @@ -248,7 +255,17 @@ namespace Barotrauma ScreenChanged = false; } - updateList.ForEach(c => c.DrawAuto(spriteBatch)); + foreach (GUIComponent c in updateList) + { + c.DrawAuto(spriteBatch); + } + + // always draw IME preview on top of everything else + foreach (GUIComponent c in updateList) + { + if (c is not GUITextBox box) { continue; } + box.DrawIMEPreview(spriteBatch); + } if (ScreenOverlayColor.A > 0.0f) { @@ -310,7 +327,7 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(10, y), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) + if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0) { y += yStep; DrawString(spriteBatch, new Vector2(10, y), @@ -1350,7 +1367,7 @@ namespace Barotrauma } } - #region Element drawing +#region Element drawing private static readonly List usedIndicatorAngles = new List(); @@ -1605,6 +1622,54 @@ namespace Barotrauma } } + public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered) + { + Vector2 topLeft = new Vector2(-origin.X, -origin.Y); + Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y); + Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y); + Vector2 actualSize = size; + + switch(outlinePos) + { + case OutlinePosition.Default: + actualSize += new Vector2(thickness); + break; + case OutlinePosition.Centered: + topLeft -= new Vector2(thickness * 0.5f); + topRight -= new Vector2(thickness * 0.5f); + bottomLeft -= new Vector2(thickness * 0.5f); + actualSize += new Vector2(thickness); + break; + case OutlinePosition.Inside: + topRight -= new Vector2(thickness, 0.0f); + bottomLeft -= new Vector2(0.0f, thickness); + break; + case OutlinePosition.Outside: + topLeft -= new Vector2(thickness); + topRight -= new Vector2(0.0f, thickness); + bottomLeft -= new Vector2(thickness, 0.0f); + actualSize += new Vector2(thickness * 2.0f); + break; + } + + Matrix rotate = Matrix.CreateRotationZ(rotation); + topLeft = Vector2.Transform(topLeft, rotate) + position; + topRight = Vector2.Transform(topRight, rotate) + position; + bottomLeft = Vector2.Transform(bottomLeft, rotate) + position; + + Rectangle srcRect = new Rectangle(0, 0, 1, 1); + sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth); + } + + public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f) + { + Rectangle srcRect = new Rectangle(0, 0, 1, 1); + sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth); + } + public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f) { DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth); @@ -1788,9 +1853,9 @@ namespace Barotrauma Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); } - #endregion +#endregion - #region Element creation +#region Element creation public static Texture2D CreateCircle(int radius, bool filled = false) { @@ -2162,9 +2227,9 @@ namespace Barotrauma return msgBox; } - #endregion +#endregion - #region Element positioning +#region Element positioning private static List CreateElements(int count, RectTransform parent, Func constructor, Vector2? relativeSize = null, Point? absoluteSize = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, @@ -2363,9 +2428,9 @@ namespace Barotrauma } } - #endregion +#endregion - #region Misc +#region Misc public static void TogglePauseMenu() { if (Screen.Selected == GameMain.MainMenuScreen) { return; } @@ -2603,6 +2668,6 @@ namespace Barotrauma if (!isSavingIndicatorEnabled) { return; } timeUntilSavingIndicatorDisabled = delay; } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 926e5520b..96a646db8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -9,6 +9,7 @@ using Barotrauma.IO; using RestSharp; using System.Net; using System.Collections.Immutable; +using Barotrauma.Tutorials; namespace Barotrauma { @@ -736,7 +737,7 @@ namespace Barotrauma public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } + if (ObjectiveManager.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); @@ -757,9 +758,9 @@ namespace Barotrauma toolTipBlock.DrawManually(spriteBatch); } - public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement, Anchor anchor = Anchor.BottomCenter, Pivot pivot = Pivot.TopLeft) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } + if (ObjectiveManager.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); @@ -774,7 +775,10 @@ namespace Barotrauma toolTipBlock.UserData = toolTip; } - toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom); + toolTipBlock.RectTransform.AbsoluteOffset = + RectTransform.CalculateAnchorPoint(anchor, targetElement) + + RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize); + if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) { toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index a8ca188cc..dd848bd65 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -111,6 +111,7 @@ namespace Barotrauma } public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } + public void ReceiveEditingInput(string text, int start, int length) { } public void ReceiveSpecialInput(Keys key) { @@ -121,9 +122,7 @@ namespace Barotrauma listBox.ReceiveSpecialInput(key); GUI.KeyboardDispatcher.Subscriber = this; break; - case Keys.Enter: - case Keys.Space: - case Keys.Escape: + default: GUI.KeyboardDispatcher.Subscriber = null; break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 71173d75d..b03bc27a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -150,7 +150,6 @@ namespace Barotrauma } } - // TODO: fix implicit hiding public override bool Selected { get { return isSelected; } @@ -1348,6 +1347,7 @@ namespace Barotrauma } public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } + public void ReceiveEditingInput(string text, int start, int length) { } public void ReceiveSpecialInput(Keys key) { @@ -1365,9 +1365,7 @@ namespace Barotrauma case Keys.Right: if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; - case Keys.Enter: - case Keys.Space: - case Keys.Escape: + default: GUI.KeyboardDispatcher.Subscriber = null; break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 3aa1cff1c..81ad4ac35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -24,7 +24,8 @@ namespace Barotrauma InGame, Vote, Hint, - Tutorial + Tutorial, + Warning // Keep this last so that it's always drawn in front } private bool IsAnimated => type == Type.InGame || type == Type.Hint || type == Type.Tutorial; @@ -84,8 +85,8 @@ namespace Barotrauma public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); - public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null) - : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize) + public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null, Type type = Type.Default) + : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize, type: type) { this.Buttons[0].OnClicked = Close; } @@ -147,7 +148,7 @@ namespace Barotrauma Tag = tag.ToIdentifier(); #warning TODO: These should be broken into separate methods at least - if (type == Type.Default || type == Type.Vote) + if (type == Type.Default || type == Type.Vote || type == Type.Warning) { Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index f3802cca6..4f938a89f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -179,6 +179,11 @@ namespace Barotrauma private set; } + /// + /// If enabled, the value wraps around to Max when you go below Min, and vice versa + /// + public bool WrapAround; + public float valueStep; private float pressedTimer; @@ -403,13 +408,19 @@ namespace Barotrauma { if (MinValueFloat != null) { - floatValue = Math.Max(floatValue, MinValueFloat.Value); - MinusButton.Enabled = floatValue > MinValueFloat; + floatValue = + WrapAround && MinValueFloat.HasValue && floatValue < MinValueFloat.Value ? + MaxValueFloat.Value : + Math.Max(floatValue, MinValueFloat.Value); + MinusButton.Enabled = WrapAround || floatValue > MinValueFloat; } if (MaxValueFloat != null) { - floatValue = Math.Min(floatValue, MaxValueFloat.Value); - PlusButton.Enabled = floatValue < MaxValueFloat; + floatValue = + WrapAround && MaxValueFloat.HasValue && floatValue > MaxValueFloat.Value ? + MinValueFloat.Value : + Math.Min(floatValue, MaxValueFloat.Value); + PlusButton.Enabled = WrapAround || floatValue < MaxValueFloat; } } @@ -417,16 +428,16 @@ namespace Barotrauma { if (MinValueInt != null && intValue < MinValueInt.Value) { - intValue = Math.Max(intValue, MinValueInt.Value); + intValue = WrapAround && MaxValueInt.HasValue ? MaxValueInt.Value : Math.Max(intValue, MinValueInt.Value); UpdateText(); } if (MaxValueInt != null && intValue > MaxValueInt.Value) { - intValue = Math.Min(intValue, MaxValueInt.Value); + intValue = WrapAround && MinValueInt.HasValue ? MinValueInt.Value : Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt; - MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt; + PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt; + MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt; } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 8ae5366b8..c9a7631cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -45,16 +46,17 @@ namespace Barotrauma } } - private ScalableFont cjkFont; + private ImmutableDictionary specialHandlingFonts; - public ScalableFont CjkFont + public ScalableFont GetFontForCategory(TextManager.SpeciallyHandledCharCategory category) { - get - { - if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } - if (font.IsCJK) { return font; } - return cjkFont; - } + if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } + if (font.SpeciallyHandledCharCategory.HasFlag(category)) { return font; } + return specialHandlingFonts.TryGetValue(category, out var resultFont) + ? resultFont + : specialHandlingFonts.TryGetValue(TextManager.SpeciallyHandledCharCategory.CJK, out resultFont) + ? resultFont + : font; } public LanguageIdentifier Language { get; private set; } @@ -70,40 +72,68 @@ namespace Barotrauma string fontPath = GetFontFilePath(element); uint size = GetFontSize(element); bool dynamicLoading = GetFontDynamicLoading(element); - bool isCJK = GetIsCJK(element); + var shcc = GetShcc(element); font?.Dispose(); - cjkFont?.Dispose(); - font = new ScalableFont(fontPath, size, GameMain.Instance.GraphicsDevice, dynamicLoading, isCJK) + specialHandlingFonts?.Values.ForEach(f => f.Dispose()); + font = new ScalableFont( + fontPath, + size, + GameMain.Instance.GraphicsDevice, + dynamicLoading, + shcc) { ForceUpperCase = element.GetAttributeBool("forceuppercase", false) }; - if (!isCJK) + + var fallbackFonts = new Dictionary(); + foreach (var flag in TextManager.SpeciallyHandledCharCategories) { - cjkFont = ExtractCjkFont(element) - ?? new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", - font.Size, GameMain.Instance.GraphicsDevice, dynamicLoading: true, isCJK: true); - cjkFont.ForceUpperCase = font.ForceUpperCase; + if (shcc.HasFlag(flag)) { continue; } + + var extractedFont = ExtractFont(flag, element); + if (extractedFont is null) { continue; } + fallbackFonts.Add(flag, extractedFont); } + fallbackFonts.Values.ForEach(ff => ff.ForceUpperCase = font.ForceUpperCase); + specialHandlingFonts = fallbackFonts.ToImmutableDictionary(); Language = GameSettings.CurrentConfig.Language; } public override void Dispose() { - font?.Dispose(); font = null; - cjkFont?.Dispose(); cjkFont = null; + font?.Dispose(); + font = null; + specialHandlingFonts?.Values.ForEach(f => f.Dispose()); + specialHandlingFonts = null; } - private ScalableFont ExtractCjkFont(ContentXElement element) + private ScalableFont ExtractFont(TextManager.SpeciallyHandledCharCategory flag, ContentXElement element) { foreach (var subElement in element.Elements().Reverse()) { if (subElement.NameAsIdentifier() != "override") { continue; } - if (subElement.GetAttributeBool("iscjk", false)) + if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag)) { return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); } } - return null; + + ScalableFont hardcodedFallback(string path) + => new ScalableFont( + path, + font.Size, + GameMain.Instance.GraphicsDevice, + dynamicLoading: true, + speciallyHandledCharCategory: flag); + + return flag switch + { + TextManager.SpeciallyHandledCharCategory.CJK + => hardcodedFallback("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf"), + TextManager.SpeciallyHandledCharCategory.Cyrillic + => hardcodedFallback("Content/Fonts/Oswald-Bold.ttf"), + _ => null + }; } private string GetFontFilePath(ContentXElement element) @@ -154,21 +184,21 @@ namespace Barotrauma return element.GetAttributeBool("dynamicloading", false); } - private bool GetIsCJK(XElement element) + private TextManager.SpeciallyHandledCharCategory GetShcc(XElement element) { foreach (var subElement in element.Elements()) { if (IsValidOverride(subElement)) { - return subElement.GetAttributeBool("iscjk", false); + return ScalableFont.ExtractShccFromXElement(subElement); } } - return element.GetAttributeBool("iscjk", false); + return ScalableFont.ExtractShccFromXElement(element); } private bool IsValidOverride(XElement element) { - if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; } + if (!element.IsOverride()) { return false; } var languages = element.GetAttributeIdentifierArray("language", Array.Empty()); return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language); } @@ -191,26 +221,26 @@ namespace Barotrauma private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); public ScalableFont GetFontForStr(string str) => - TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font; + Prefabs.ActivePrefab.GetFontForCategory(TextManager.GetSpeciallyHandledCategories(str)); - public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth) { - DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth); + DrawString(sb, text.Value, position, color, rotation, origin, scale, spriteEffects, layerDepth); } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth) { - GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth); + GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth); } - public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft) + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft) { - DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth, alignment); + DrawString(sb, text.Value, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment); } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, alignment, forceUpperCase); + GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment, forceUpperCase); } public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) @@ -223,9 +253,9 @@ namespace Barotrauma GetFontForStr(text).DrawString(sb, text, position, color, forceUpperCase, italics); } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); + GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); } public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 99461d746..168f53a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Barotrauma @@ -285,8 +286,8 @@ namespace Barotrauma /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. /// - public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) + public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) : base(style, rectT) { if (color.HasValue) @@ -551,6 +552,8 @@ namespace Barotrauma if (TextGetter != null) { Text = TextGetter(); } + string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; if (overflowClipActive) { @@ -561,7 +564,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - if (!text.IsNullOrEmpty()) + if (!string.IsNullOrEmpty(textToShow)) { Vector2 pos = rect.Location.ToVector2() + textPos + TextOffset; if (RoundToNearestPixel) @@ -570,7 +573,8 @@ namespace Barotrauma pos.Y = (int)pos.Y; } - Color currentTextColor = State == ComponentState.Hover || State == ComponentState.HoverSelected ? HoverTextColor : TextColor; + Color currentTextColor = State is ComponentState.Hover or ComponentState.HoverSelected ? HoverTextColor : TextColor; + if (!enabled) { currentTextColor = disabledTextColor; @@ -582,8 +586,14 @@ namespace Barotrauma if (!HasColorHighlight) { - string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); + if (TextManager.DebugDraw) + { + if (!text.NestedStr.Loaded || text.NestedStr.Language == LanguageIdentifier.None) + { + colorToShow = Color.Magenta; + } + } if (Shadow) { @@ -597,10 +607,10 @@ namespace Barotrauma { if (OverrideRichTextDataAlpha) { - RichTextData.Value.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); + RichTextData?.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); } - Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text.SanitizedString).Value, pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData.Value, alignment: textAlignment, forceUpperCase: ForceUpperCase); + Font.DrawStringWithColors(spriteBatch, textToShow, pos, + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData, alignment: textAlignment, forceUpperCase: ForceUpperCase); } Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 1bd3f88eb..05d2898e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -11,7 +11,7 @@ namespace Barotrauma public delegate void TextBoxEvent(GUITextBox sender, Keys key); - public class GUITextBox : GUIComponent, IKeyboardSubscriber + public partial class GUITextBox : GUIComponent, IKeyboardSubscriber { public event TextBoxEvent OnSelected; public event TextBoxEvent OnDeselected; @@ -77,12 +77,12 @@ namespace Barotrauma private int selectionEndIndex; private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex; - private GUICustomComponent caretAndSelectionRenderer; + private readonly GUICustomComponent caretAndSelectionRenderer; private bool mouseHeldInside; private readonly Memento memento = new Memento(); - + // Skip one update cycle, fixes Enter key instantly deselecting the chatbox private bool skipUpdate; @@ -199,6 +199,7 @@ namespace Barotrauma base.Font = value; if (textBlock == null) { return; } textBlock.Font = value; + imePreviewTextHandler.Font = Font; } } @@ -263,6 +264,10 @@ namespace Barotrauma public override bool PlaySoundOnSelect { get; set; } = true; + private readonly IMEPreviewTextHandler imePreviewTextHandler; + + public bool IsIMEActive => imePreviewTextHandler is { HasText: true }; + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) @@ -274,6 +279,7 @@ namespace Barotrauma frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap); + imePreviewTextHandler = new IMEPreviewTextHandler(textBlock.Font); GUIStyle.Apply(textBlock, "", this); if (font != null) { textBlock.Font = font; } CaretEnabled = true; @@ -305,18 +311,17 @@ namespace Barotrauma textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - clearButtonWidth - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } Font = textBlock.Font; - Enabled = true; - rectT.SizeChanged += () => + rectT.SizeChanged += () => { if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } - caretPosDirty = true; + caretPosDirty = true; }; rectT.ScaleChanged += () => { if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } - caretPosDirty = true; + caretPosDirty = true; }; } @@ -391,14 +396,16 @@ namespace Barotrauma { GUI.KeyboardDispatcher.Subscriber = null; } + OnDeselected?.Invoke(this, Keys.None); + imePreviewTextHandler.Reset(); } public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectOffset = null) { frame.Flash(color, flashDuration, useRectangleFlash, useCircularFlash, flashRectOffset); } - + protected override void Update(float deltaTime) { if (!Visible) return; @@ -579,6 +586,7 @@ namespace Barotrauma { CaretIndex = Math.Min(Text.Length, CaretIndex + input.Length); OnTextChanged?.Invoke(this, Text); + imePreviewTextHandler?.Reset(); } } @@ -607,10 +615,12 @@ namespace Barotrauma public void ReceiveCommandInput(char command) { - if (Text == null) Text = ""; + if (IsIMEActive) { return; } + + if (Text == null) { Text = ""; } // Prevent alt gr from triggering any of these as that combination is often needed for special characters - if (PlayerInput.IsAltDown()) return; + if (PlayerInput.IsAltDown()) { return; } switch (command) { @@ -684,8 +694,21 @@ namespace Barotrauma } } + public void ReceiveEditingInput(string text, int start, int length) + { + if (string.IsNullOrEmpty(text)) + { + imePreviewTextHandler.Reset(); + return; + } + + imePreviewTextHandler.UpdateText(text, start, length); + } + public void ReceiveSpecialInput(Keys key) { + if (IsIMEActive) { return; } + switch (key) { case Keys.Left: @@ -874,6 +897,11 @@ namespace Barotrauma } } + public void DrawIMEPreview(SpriteBatch spriteBatch) + { + imePreviewTextHandler.DrawIMEPreview(spriteBatch, CaretScreenPos, textBlock); + } + private void CalculateSelection() { string textDrawn = Censor ? textBlock.CensoredText : WrappedText; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index ded2b870e..dec3485bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -65,7 +65,7 @@ namespace Barotrauma get; private set; } - public static Rectangle AfflictionAreaLeft + public static Rectangle HealthBarAfflictionArea { get; private set; } @@ -143,7 +143,7 @@ namespace Barotrauma } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); - AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); int messageAreaWidth = GameMain.GraphicsWidth / 3; @@ -173,7 +173,7 @@ namespace Barotrauma int objectiveListAreaX = HealthWindowAreaLeft.Right + Padding; int objectiveListAreaY = ButtonAreaTop.Bottom + Padding; - TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (AfflictionAreaLeft.Top - Padding) - objectiveListAreaY); + TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (HealthBarAfflictionArea.Top - Padding) - objectiveListAreaY); int votingAreaWidth = (int)(400 * GUI.Scale); int votingAreaX = GameMain.GraphicsWidth - Padding - votingAreaWidth; @@ -193,7 +193,7 @@ namespace Barotrauma DrawRectangle(CrewArea, Color.Blue * 0.5f); DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(AfflictionAreaLeft, Color.Red * 0.5f); + DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f); DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs new file mode 100644 index 000000000..3e57935de --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs @@ -0,0 +1,90 @@ +#nullable enable + +using System.Collections.Immutable; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + + public sealed class IMEPreviewTextHandler + { + public bool HasText => !string.IsNullOrEmpty(previewText); + + // This has to be settable because for some reason we update the font of GUITextBox in some places + public GUIFont Font { get; set; } + + private string previewText = string.Empty; + private Vector2 textSize; + + private bool isSectioned; + private ImmutableArray? richTextData; + + public IMEPreviewTextHandler(GUIFont font) + { + Font = font; + } + + public void Reset() + { + textSize = Vector2.Zero; + previewText = string.Empty; + richTextData = null; + isSectioned = false; + } + + public void UpdateText(string text, int start, int length) + { + isSectioned = start >= 0 && length > 0; + richTextData = null; + + if (string.IsNullOrEmpty(text)) + { + Reset(); + return; + } + + previewText = text; + + textSize = Font.MeasureString(text); + + if (!isSectioned) { return; } + + string coloredText = ToolBox.ColorSectionOfString(text, start, length, GUIStyle.Orange); + + RichString richString = RichString.Rich(coloredText); + + previewText = richString.SanitizedValue; + richTextData = richString.RichTextData; + } + + public void DrawIMEPreview(SpriteBatch spriteBatch, Vector2 position, GUITextBlock textBlock) + { + if (!HasText) { return; } + + int inflate = GUI.IntScale(3); + + RectangleF rect = new RectangleF(position, textSize); + rect.Inflate(inflate, inflate); + + RectangleF borderRect = rect; + borderRect.Inflate(1, 1); + + GUI.DrawFilledRectangle(spriteBatch, borderRect, Color.White, depth: 0.02f); + GUI.DrawFilledRectangle(spriteBatch, rect, Color.Black, depth: 0.01f); + + Font.DrawStringWithColors(spriteBatch, + text: previewText, + position: position, + color: isSectioned ? GUIStyle.TextColorNormal : GUIStyle.Orange, + rotation: 0.0f, + origin: Vector2.Zero, + scale: 1f, + spriteEffects: SpriteEffects.None, + layerDepth: 0, + richTextData: richTextData, + alignment: textBlock.TextAlignment, + forceUpperCase: textBlock.ForceUpperCase); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 54373bcff..6f5272743 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -12,7 +12,7 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; namespace Barotrauma { [SuppressMessage("ReSharper", "UnusedVariable")] - internal class MedicalClinicUI + internal sealed class MedicalClinicUI { private enum ElementState { @@ -127,12 +127,14 @@ namespace Barotrauma { public readonly GUIComponent Panel; public readonly GUIListBox HealList; + public readonly GUIComponent TreatAllButton; public readonly List HealElements; - public CrewHealList(GUIListBox healList, GUIComponent panel) + public CrewHealList(GUIListBox healList, GUIComponent panel, GUIComponent treatAllButton) { Panel = panel; HealList = healList; + TreatAllButton = treatAllButton; HealElements = new List(); } } @@ -179,7 +181,7 @@ namespace Barotrauma private PopupAfflictionList? selectedCrewAfflictionList; private bool isWaitingForServer; private const float refreshTimerMax = 3f; - private float refreshTimer = 0; + private float refreshTimer; private PlayerBalanceElement? playerBalanceElement; @@ -196,7 +198,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(0.2f, 0.1f), parent.RectTransform, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!") { - OnClicked = (_, __) => + OnClicked = (_, _) => { parent.ClearChildren(); CreateUI(); @@ -254,7 +256,7 @@ namespace Barotrauma continue; } - CreatePendingHealElement(healList.HealList.Content, crewMember, healList, Array.Empty()); + CreatePendingHealElement(healList.HealList.Content, crewMember, healList, ImmutableArray.Empty); } // check if there are elements that the crew doesn't have @@ -309,7 +311,7 @@ namespace Barotrauma private void UpdateCrewPanel() { - if (!(crewHealList is { } healList)) { return; } + if (crewHealList is not { } healList) { return; } ImmutableArray crew = MedicalClinic.GetCrewCharacters(); @@ -334,12 +336,21 @@ namespace Barotrauma healList.HealList.Content.RemoveChild(element.UIElement); } - IEnumerable orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100); + IEnumerable orderedList = healList.HealElements.OrderBy(static element => element.Target.Character?.HealthPercentage ?? 100); foreach (CrewElement element in orderedList) { element.UIElement.SetAsLastChild(); } + + healList.TreatAllButton.Enabled = false; + foreach (CrewElement element in healList.HealElements) + { + if (element.Afflictions.Count is 0) { continue; } + + healList.TreatAllButton.Enabled = true; + break; + } } private static void UpdateAfflictionList(CrewElement healElement) @@ -350,7 +361,7 @@ namespace Barotrauma // sum up all the afflictions and their strengths Dictionary afflictionAndStrength = new Dictionary(); - foreach (Affliction affliction in health.GetAllAfflictions().Where(a => MedicalClinic.IsHealable(a))) + foreach (Affliction affliction in health.GetAllAfflictions().Where(MedicalClinic.IsHealable)) { if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength)) { @@ -446,8 +457,8 @@ namespace Barotrauma }; GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); - GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); + new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); + new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); @@ -480,22 +491,24 @@ namespace Barotrauma Stretch = true }; - // GUILayoutGroup sortLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.05f), clinicContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - - // new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), sortLayout.RectTransform), TextManager.Get("campaignstore.sortby"), font: GUI.SubHeadingFont); - - // GUIDropDown sortDropdown = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1f), sortLayout.RectTransform)); - // - // foreach (SortMode mode in Enum.GetValues(typeof(SortMode)).Cast()) - // { - // sortDropdown.AddItem(TextManager.Get($"medicalclinic.sortmode.{mode}"), mode); - // } - // - // sortDropdown.SelectItem(SortMode.Severity); - GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, clinicContainer.RectTransform)); - crewHealList = new CrewHealList(crewList, parent); + GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) + { + OnClicked = (_, _) => + { + isWaitingForServer = true; + medicalClinic.TreatAllButtonAction(OnReceived); + return true; + } + }; + + crewHealList = new CrewHealList(crewList, parent, treatAllButton); + + void OnReceived(MedicalClinic.CallbackOnlyRequest obj) + { + isWaitingForServer = false; + } } private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) @@ -525,9 +538,9 @@ namespace Barotrauma TextColor = GUIStyle.Red }; - MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; + MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember(info); - crewBackground.OnClicked = (_, __) => + crewBackground.OnClicked = (_, _) => { SelectCharacter(member, new Vector2(panel.Rect.Right, crewBackground.Rect.Top)); return true; @@ -618,7 +631,7 @@ namespace Barotrauma pendingHealList = list; } - private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, MedicalClinic.NetAffliction[] afflictions) + private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, ImmutableArray afflictions) { CharacterInfo? healInfo = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); if (healInfo is null) { return; } @@ -803,7 +816,7 @@ namespace Barotrauma } allComponents.Add(treatAllButton); - treatAllButton.OnClicked = (_, __) => + treatAllButton.OnClicked = (_, _) => { ImmutableArray afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); if (!afflictions.Any()) { return true; } @@ -845,9 +858,9 @@ namespace Barotrauma GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); - Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, (int)affliction.AfflictionSeverity / 2f); + Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); - LocalizedString vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); + LocalizedString vitalityText = affliction.VitalityDecrease == 0 ? string.Empty : TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.VitalityDecrease).ToString()); GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center) { TextColor = textColor, @@ -856,7 +869,7 @@ namespace Barotrauma AutoScaleHorizontal = true }; - LocalizedString severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); + LocalizedString severityText = Affliction.GetStrengthText(affliction.Strength, affliction.Prefab.MaxStrength); GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { TextColor = textColor, @@ -873,9 +886,13 @@ namespace Barotrauma { RelativeSpacing = 0.05f }; - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), prefab.Description, font: GUIStyle.SmallFont, wrap: true) + LocalizedString description = affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.OtherCharacter); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), + description, + font: GUIStyle.SmallFont, + wrap: true) { - ToolTip = prefab.Description + ToolTip = description }; bool truncated = false; while (descriptionBlock.TextSize.Y > descriptionBlock.Rect.Height && descriptionBlock.WrappedText.Contains('\n')) @@ -919,10 +936,9 @@ namespace Barotrauma } else { - MedicalClinic.NetCrewMember newMember = new MedicalClinic.NetCrewMember + MedicalClinic.NetCrewMember newMember = crewMember with { - CharacterInfoID = crewMember.CharacterInfoID, - Afflictions = Array.Empty() + Afflictions = ImmutableArray.Empty }; existingMember = newMember; @@ -936,7 +952,7 @@ namespace Barotrauma } } - existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToArray(); + existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); ToggleElements(ElementState.Disabled, elementsToDisable); medicalClinic.AddPendingButtonAction(existingMember, request => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 4f87042dd..7eb393015 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -134,7 +134,7 @@ namespace Barotrauma set => hadSellSubPermissions = value; } - private bool HasPermissionToUseTab(StoreTab tab) + private static bool HasPermissionToUseTab(StoreTab tab) { return tab switch { @@ -278,6 +278,7 @@ namespace Barotrauma RefreshBuying(updateOwned: false); RefreshSelling(updateOwned: false); RefreshSellingFromSub(updateOwned: false); + SetConfirmButtonBehavior(); needsRefresh = false; } @@ -475,6 +476,7 @@ namespace Barotrauma }; List itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast().ToList(); + itemCategories.Remove(MapEntityCategory.None); //don't show categories with no buyable items itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought)); itemCategoryButtons.Clear(); @@ -507,6 +509,7 @@ namespace Barotrauma { btn.RectTransform.SizeChanged += () => { + if (btn.Frame.sprites == null) { return; } var sprite = btn.Frame.sprites[GUIComponent.ComponentState.None].First(); btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); }; @@ -617,9 +620,9 @@ namespace Barotrauma Stretch = true }; var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null); - shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; - shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; - shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; + shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true }; + shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true }; + shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true }; var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) { @@ -745,7 +748,7 @@ namespace Barotrauma } ?? Enumerable.Empty(); foreach (var button in itemCategoryButtons) { - if (!(button.UserData is MapEntityCategory category)) + if (button.UserData is not MapEntityCategory category) { continue; } @@ -859,18 +862,17 @@ namespace Barotrauma float prevBuyListScroll = storeBuyList.BarScroll; float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; - int dailySpecialCount = ActiveStore.DailySpecials.Count; - if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) + int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0; + if ((ActiveStore == null && storeDailySpecialsGroup != null) || (storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { - if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) + storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); + if (ActiveStore != null && (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount)) { - storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount); storeDailySpecialsGroup.Parent.SetAsFirstChild(); } else { - storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent); storeDailySpecialsGroup = null; } storeBuyList.RecalculateChildren(); @@ -879,20 +881,22 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(StoreTab.Buy); var existingItemFrames = new HashSet(); - foreach (PurchasedItem item in ActiveStore.Stock) + if (ActiveStore != null) { - CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); - } - - foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) - { - if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } - CreateOrUpdateItemFrame(itemPrefab, 0); + foreach (PurchasedItem item in ActiveStore.Stock) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) + { + if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } + CreateOrUpdateItemFrame(itemPrefab, 0); + } } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) { - if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo)) + if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy()) { bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? @@ -945,11 +949,11 @@ namespace Barotrauma float prevSellListScroll = storeSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; - int requestedGoodsCount = ActiveStore.RequestedGoods.Count; - if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount) + int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0; + if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount) { storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent); - if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount) + if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount)) { storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount); storeRequestedGoodGroup.Parent.SetAsFirstChild(); @@ -964,14 +968,17 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(StoreTab.Sell); var existingItemFrames = new HashSet(); - foreach (PurchasedItem item in itemsToSell) + if (ActiveStore != null) { - CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); - } - foreach (var requestedGood in ActiveStore.RequestedGoods) - { - if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } - CreateOrUpdateItemFrame(requestedGood, 0); + foreach (PurchasedItem item in itemsToSell) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + foreach (var requestedGood in ActiveStore.RequestedGoods) + { + if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } + CreateOrUpdateItemFrame(requestedGood, 0); + } } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) @@ -1029,11 +1036,11 @@ namespace Barotrauma float prevSellListScroll = storeSellFromSubList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; - int requestedGoodsCount = ActiveStore.RequestedGoods.Count; - if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount) + int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0; + if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount) { storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent); - if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount) + if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount)) { storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount); storeRequestedSubGoodGroup.Parent.SetAsFirstChild(); @@ -1048,14 +1055,17 @@ namespace Barotrauma bool hasPermissions = HasSellSubPermissions; var existingItemFrames = new HashSet(); - foreach (PurchasedItem item in itemsToSellFromSub) + if (ActiveStore != null) { - CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); - } - foreach (var requestedGood in ActiveStore.RequestedGoods) - { - if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } - CreateOrUpdateItemFrame(requestedGood, 0); + foreach (PurchasedItem item in itemsToSellFromSub) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + foreach (var requestedGood in ActiveStore.RequestedGoods) + { + if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } + CreateOrUpdateItemFrame(requestedGood, 0); + } } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) @@ -1110,7 +1120,7 @@ namespace Barotrauma private void SetPriceGetters(GUIComponent itemFrame, bool buying) { - if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } + if (itemFrame == null || itemFrame.UserData is not PurchasedItem pi) { return; } if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) { @@ -1142,6 +1152,7 @@ namespace Barotrauma public void RefreshItemsToSell() { itemsToSell.Clear(); + if (ActiveStore == null) { return; } var playerItems = CargoManager.GetSellableItems(Character.Controlled); foreach (Item playerItem in playerItems) { @@ -1172,6 +1183,7 @@ namespace Barotrauma public void RefreshItemsToSellFromSub() { itemsToSellFromSub.Clear(); + if (ActiveStore == null) { return; } var subItems = CargoManager.GetSellableItemsFromSub(); foreach (Item subItem in subItems) { @@ -1205,52 +1217,55 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(tab); HashSet existingItemFrames = new HashSet(); int totalPrice = 0; - foreach (PurchasedItem item in items) + if (ActiveStore != null) { - if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } - GUINumberInput numInput = null; - if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) + foreach (PurchasedItem item in items) { - itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); - numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; - } - else - { - itemFrame.UserData = item; - numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } + GUINumberInput numInput = null; + if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) + { + itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); + numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + } + else + { + itemFrame.UserData = item; + numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + if (numInput != null) + { + numInput.UserData = item; + numInput.Enabled = hasPermissions; + numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); + } + SetOwnedText(itemFrame); + SetItemFrameStatus(itemFrame, hasPermissions); + } + existingItemFrames.Add(itemFrame); + + suppressBuySell = true; if (numInput != null) { - numInput.UserData = item; - numInput.Enabled = hasPermissions; - numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); + if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); } + numInput.IntValue = item.Quantity; } - SetOwnedText(itemFrame); - SetItemFrameStatus(itemFrame, hasPermissions); - } - existingItemFrames.Add(itemFrame); + suppressBuySell = false; - suppressBuySell = true; - if (numInput != null) - { - if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); } - numInput.IntValue = item.Quantity; - } - suppressBuySell = false; - - try - { - int price = tab switch + try { - StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), - StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), - StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), - _ => throw new NotImplementedException() - }; - totalPrice += item.Quantity * price; - } - catch (NotImplementedException e) - { - DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + int price = tab switch + { + StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + _ => throw new NotImplementedException() + }; + totalPrice += item.Quantity * price; + } + catch (NotImplementedException e) + { + DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + } } } @@ -1287,7 +1302,7 @@ namespace Barotrauma private void SortItems(GUIListBox list, SortingMethod sortingMethod) { - if (CurrentLocation == null) { return; } + if (CurrentLocation == null || ActiveStore == null) { return; } if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc) { @@ -1662,13 +1677,15 @@ namespace Barotrauma { OwnedItems.Clear(); + if (ActiveStore == null) { return; } + // Add items on the sub(s) if (Submarine.MainSub?.GetItems(true) is List subItems) { foreach (var subItem in subItems) { - if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; } - if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; } + if (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; } + if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } AddOwnedItem(subItem); } @@ -1701,7 +1718,7 @@ namespace Barotrauma void AddOwnedItem(Item item) { - if (!(item?.Prefab.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)) { return; } + if (item?.Prefab.GetPriceInfo(ActiveStore) is not PriceInfo priceInfo) { return; } bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { @@ -1729,7 +1746,7 @@ namespace Barotrauma private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled) { - if (!(itemFrame?.UserData is PurchasedItem pi)) { return; } + if (itemFrame?.UserData is not PurchasedItem pi) { return; } bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled; if (!refreshFrameStatus) { return; } if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon) @@ -1841,11 +1858,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.Name; - if (!purchasedItem.ItemPrefab.Description.IsNullOrEmpty()) - { - toolTip += $"\n{purchasedItem.ItemPrefab.Description}"; - } + toolTip = purchasedItem.ItemPrefab.GetTooltip(); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) @@ -1859,7 +1872,7 @@ namespace Barotrauma } } } - itemComponent.ToolTip = toolTip; + itemComponent.ToolTip = RichString.Rich(toolTip); } if (ownedLabel != null) { @@ -1995,11 +2008,23 @@ namespace Barotrauma int totalPrice = 0; foreach (var item in itemsToPurchase) { - if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) + if (item is null) { continue; } + + if (item.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) { itemsToRemove.Add(item); continue; } + + if (item.ItemPrefab.DefaultPrice.RequiresUnlock) + { + if (!CargoManager.HasUnlockedStoreItem(item.ItemPrefab)) + { + itemsToRemove.Add(item); + continue; + } + } + totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); @@ -2054,7 +2079,12 @@ namespace Barotrauma private void SetShoppingCrateTotalText() { - if (IsBuying) + if (ActiveStore == null) + { + shoppingCrateTotal.Text = TextManager.FormatCurrency(0); + shoppingCrateTotal.TextColor = Color.White; + } + else if (IsBuying) { shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal); shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White; @@ -2074,7 +2104,11 @@ namespace Barotrauma private void SetConfirmButtonBehavior() { - if (IsBuying) + if (ActiveStore == null) + { + confirmButton.OnClicked = null; + } + else if (IsBuying) { confirmButton.ClickSound = GUISoundType.ConfirmTransaction; confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); @@ -2102,6 +2136,7 @@ namespace Barotrauma private void SetConfirmButtonStatus() { confirmButton.Enabled = + ActiveStore != null && HasActiveTabPermissions() && ActiveShoppingCrateList.Content.RectTransform.Children.Any() && activeTab switch @@ -2111,6 +2146,7 @@ namespace Barotrauma StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance, _ => false }; + confirmButton.Visible = ActiveStore != null; } private void SetClearAllButtonStatus() @@ -2169,7 +2205,7 @@ namespace Barotrauma { needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count || itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || - itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + itemsToSellFromSub.Any(i => prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is not PurchasedItem prev || i.Quantity != prev.Quantity) || prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); } } @@ -2184,29 +2220,32 @@ namespace Barotrauma prevBalance = currBalance; } } - if (needsItemsToSellRefresh) + if (ActiveStore != null) { - RefreshItemsToSell(); - } - if (needsItemsToSellFromSubRefresh) - { - RefreshItemsToSellFromSub(); - } - if (needsRefresh) - { - Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); - } - if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) - { - RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); - } - if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) - { - RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); - } - if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) - { - RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); + if (needsItemsToSellRefresh) + { + RefreshItemsToSell(); + } + if (needsItemsToSellFromSubRefresh) + { + RefreshItemsToSellFromSub(); + } + if (needsRefresh) + { + Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) + { + RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) + { + RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) + { + RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); + } } updateStopwatch.Stop(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 7a9a97fc5..21481ef4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -472,7 +472,7 @@ namespace Barotrauma if (transferService) { subsToShow.AddRange(GameMain.GameSession.OwnedSubmarines); - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + subsToShow.Sort(ComparePrice); string currentSubName = CurrentOrPendingSubmarine().Name; int currentIndex = subsToShow.FindIndex(s => s.Name == currentSubName); if (currentIndex != -1) @@ -484,7 +484,11 @@ namespace Barotrauma { subsToShow.AddRange((GameMain.Client is null ? SubmarineInfo.SavedSubmarines : MultiPlayerCampaign.GetCampaignSubs()) .Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + if (GameMain.GameSession.Campaign?.Map?.CurrentLocation is Location currentLocation) + { + subsToShow.RemoveAll(sub => !currentLocation.IsSubmarineAvailable(sub)); + } + subsToShow.Sort(ComparePrice); } if (transferService) @@ -492,10 +496,14 @@ namespace Barotrauma SetConfirmButtonState(selectedSubmarine != null && selectedSubmarine.Name != CurrentOrPendingSubmarine().Name); } - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); pageCount = Math.Max(1, (int)Math.Ceiling(subsToShow.Count / (float)submarinesPerPage)); UpdatePaging(); ContentRefreshRequired = false; + + static int ComparePrice(SubmarineInfo x, SubmarineInfo y) + { + return x.Price.CompareTo(y.Price) * 100 + x.Name.CompareTo(y.Name); + } } private SubmarineInfo GetSubToDisplay(int index) @@ -673,7 +681,7 @@ namespace Barotrauma { if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) { - return Submarine.MainSub.Info; + return Submarine.MainSub?.Info; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 295818c7b..781483117 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -34,7 +34,7 @@ namespace Barotrauma private List teamIDs; private const string inLobbyString = "\u2022 \u2022 \u2022"; - private GUIFrame pendingChangesFrame = null; + public static GUIFrame PendingChangesFrame = null; public static Color OwnCharacterBGColor = Color.Gold * 0.7f; private bool isTransferMenuOpen; @@ -44,6 +44,7 @@ namespace Barotrauma private float transferMenuOpenState; private bool transferMenuStateCompleted; private readonly HashSet registeredEvents = new HashSet(); + private readonly TalentMenu talentMenu = new TalentMenu(); private class LinkedGUI { @@ -206,15 +207,8 @@ namespace Barotrauma transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height); } GameSession.UpdateTalentNotificationIndicator(talentPointNotification); - if (Character.Controlled?.Info is { } characterInfo && talentResetButton != null && talentApplyButton != null) - { - int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); - talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; - if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) - { - talentApplyButton.Flash(GUIStyle.Orange); - } - } + + talentMenu?.Update(); if (SelectedTab != InfoFrameTab.Crew) { return; } if (linkedGUIList == null) { return; } @@ -325,11 +319,11 @@ namespace Barotrauma AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) }, style: null); - pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); + PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false) { - NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame); } SetBalanceText(balanceText, campaignMode.Bank.Balance); @@ -403,7 +397,7 @@ namespace Barotrauma CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; case InfoFrameTab.Talents: - CreateCharacterInfo(infoFrameHolder); + talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character); break; } } @@ -957,16 +951,26 @@ namespace Barotrauma if (character != null) { - if (GameMain.Client == null) + if (GameMain.Client is null) { GUIComponent preview = character.Info.CreateInfoFrame(background, false, null); } else { GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); + GameMain.Client.SelectCrewCharacter(character, preview); if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); } } + + if (background.FindChild(TalentMenu.ManageBotTalentsButtonUserData, recursive: true) is GUIButton { Enabled: true } talentButton) + { + talentButton.OnClicked = (button, o) => + { + talentMenu.CreateGUI(infoFrameHolder, character); + return true; + }; + } } else if (client != null) { @@ -1487,7 +1491,11 @@ namespace Barotrauma GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = (int)(0.0245f * missionFrame.Rect.Height); GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null); - Location location = GameMain.GameSession.EndLocation ?? GameMain.GameSession.StartLocation; + Location location = GameMain.GameSession.StartLocation; + if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) + { + location ??= GameMain.GameSession.EndLocation; + } GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform)) { @@ -1774,373 +1782,10 @@ namespace Barotrauma sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); } } - private Color unselectedColor = new Color(240, 255, 255, 225); - private Color unselectableColor = new Color(100, 100, 100, 225); - private Color pressedColor = new Color(60, 60, 60, 225); - - private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>(); - private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); - private List selectedTalents = new List(); - - private GUITextBlock experienceText; - private GUIProgressBar experienceBar; - private GUITextBlock talentPointText; - private GUIListBox skillListBox; - - private GUIButton talentApplyButton, - talentResetButton; private GUIImage talentPointNotification; - private readonly ImmutableDictionary talentStageStyles = new Dictionary - { - { TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") }, - { TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") }, - { TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") }, - }.ToImmutableDictionary(); - - private readonly ImmutableDictionary talentStageBackgroundColors = new Dictionary - { - { TalentTree.TalentTreeStageState.Invalid, new Color(48,48,48,255) }, - { TalentTree.TalentTreeStageState.Locked, new Color(48,48,48,255) }, - { TalentTree.TalentTreeStageState.Unlocked, new Color(24,37,31,255) }, - { TalentTree.TalentTreeStageState.Available, new Color(50,47,33,255) }, - { TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) }, - }.ToImmutableDictionary(); - - private void CreateCharacterInfo(GUIFrame infoFrame) - { - infoFrame.ClearChildren(); - talentButtons.Clear(); - talentCornerIcons.Clear(); - - GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); - int padding = GUI.IntScale(15); - GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); - - GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null); - - GUIFrame characterSettingsFrame = null; - GUILayoutGroup characterLayout = null; - if (!(GameMain.NetworkMember is null)) - { - characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: null) { Visible = false }; - characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); - GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); - GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); - GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); - } - - Character controlledCharacter = Character.Controlled; - CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo; - if (info == null) { return; } - - Job job = info.Job; - - GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) - { - AbsoluteSpacing = GUI.IntScale(10), - Stretch = true - }; - - GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), contentLayout.RectTransform, Anchor.Center), isHorizontal: true); - - new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => - { - float posY = component.Rect.Center.Y - component.Rect.Width / 2; - info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false); - }); - - GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) - { - AbsoluteSpacing = GUI.IntScale(5), - CanBeFocused = true - }; - - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); - - if (!info.OmitJobInMenus) - { - nameBlock.TextColor = job.Prefab.UIColor; - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; - } - - if (info.PersonalityTrait != null) - { - LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName); - Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); - GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); - traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); - } - - IEnumerable talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); - if (talentsOutsideTree.Count() > 0) - { - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null); - - GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); - - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont); - talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y); - - var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) - { - AutoHideScrollBar = false, - ResizeContentToMakeSpaceForScrollBar = false - }; - extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter); - extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65)); - extraTalentLayout.Recalculate(); - extraTalentList.ForceLayoutRecalculation(); - - foreach (var extraTalent in talentsOutsideTree) - { - var img = new GUIImage(new RectTransform(new Point(extraTalentList.Content.Rect.Height), extraTalentList.Content.RectTransform), sprite: extraTalent.Icon, scaleToFit: true) - { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description), - Color = GUIStyle.Green - }; - img.RectTransform.SizeChanged += () => - { - img.RectTransform.MaxSize = new Point(img.Rect.Height); - }; - } - } - - GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight) - { - AbsoluteSpacing = GUI.IntScale(5), - Stretch = true - }; - - GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont); - - skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); - CreateSkillList(controlledCharacter, info, skillListBox); - - new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine"); - - GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); - - if (controlledCharacter == null) - { - talentTreeListBox.Enabled = false; - } - else - { - if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - selectedTalents = info.GetUnlockedTalentsInTree().ToList(); - - List subTreeNames = new List(); - foreach (var subTree in talentTree.TalentSubTrees) - { - GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); - GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); - - GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - int elementPadding = GUI.IntScale(8); - Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; - GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); - subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); - - for (int i = 0; i < 4; i++) - { - GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - - Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; - - GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground") - { - Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] - }; - GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; - - GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) - { - CanBeFocused = false, - Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] - }; - - Point iconSize = cornerIcon.RectTransform.NonScaledSize; - cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); - - if (subTree.TalentOptionStages.Length <= i) { continue; } - - TalentOption talentOption = subTree.TalentOptionStages[i]; - GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); - GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - foreach (Identifier talentId in talentOption.TalentIdentifiers.OrderBy(t => t)) - { - if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talent)) { continue; } - GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) - { - CanBeFocused = false - }; - - GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); - GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) - { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description), - UserData = talent.Identifier, - PressedColor = pressedColor, - Enabled = controlledCharacter != null, - OnClicked = (button, userData) => - { - // deselect other buttons in tier by removing their selected talents from pool - foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) - { - if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button) - { - if (!controlledCharacter.HasTalent(otherTalentIdentifier)) - { - selectedTalents.Remove(otherTalentIdentifier); - } - } - } - Identifier talentIdentifier = (Identifier)userData; - - if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents)) - { - if (!selectedTalents.Contains(talentIdentifier)) - { - selectedTalents.Add(talentIdentifier); - } - } - else if (!controlledCharacter.HasTalent(talentIdentifier)) - { - selectedTalents.Remove(talentIdentifier); - } - - UpdateTalentInfo(); - return true; - }, - }; - - talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; - - GUIComponent iconImage; - if (talent.Icon is null) - { - iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null) - { - OutlineColor = GUIStyle.Red, - TextColor = GUIStyle.Red, - PressedColor = unselectableColor, - DisabledColor = unselectableColor, - CanBeFocused = false, - }; - } - else - { - iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) - { - PressedColor = unselectableColor, - DisabledColor = unselectableColor * 0.5f, - CanBeFocused = false, - }; - } - iconImage.Enabled = talentButton.Enabled; - talentButtons.Add((talentButton, iconImage)); - } - talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); - } - } - GUITextBlock.AutoScaleAndNormalize(subTreeNames); - - GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true) - { - RelativeSpacing = 0.01f, - Stretch = true - }; - - GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform)); - GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); - - experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), - barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) - { - IsHorizontal = true, - }; - - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) - { - Shadow = true, - ToolTip = TextManager.Get("experiencetooltip") - }; - - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; - - talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") - { - OnClicked = ResetTalentSelection - }; - talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") - { - OnClicked = ApplyTalentSelection, - }; - GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); - } - - if (!(GameMain.NetworkMember is null)) - { - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), - text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") - { - IgnoreLayoutGroups = false - }; - newCharacterBox.TextBlock.AutoScaleHorizontal = true; - - newCharacterBox.OnClicked = (button, o) => - { - if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) - { - GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => - { - newCharacterBox.Text = TextManager.Get("settings"); - if (pendingChangesFrame != null) - { - NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); - } - OpenMenu(); - }); - return true; - } - - OpenMenu(); - return true; - - void OpenMenu() - { - characterSettingsFrame!.Visible = true; - content.Visible = false; - } - }; - - if (!(characterLayout is null)) - { - GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); - new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? - { - OnClicked = (button, o) => - { - GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); - characterSettingsFrame!.Visible = false; - content.Visible = true; - return true; - } - }; - } - } - - UpdateTalentInfo(); - } - - private void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) + public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) { parent.Content.ClearChildren(); List skillNames = new List(); @@ -2154,10 +1799,10 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight); - float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; + float modifiedSkillLevel = MathF.Floor(character?.GetSkillLevel(skill.Identifier) ?? skill.Level); if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { - int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); + int skillChange = (int)MathF.Floor(modifiedSkillLevel - MathF.Floor(skill.Level)); string stringColor = skillChange switch { > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), @@ -2168,123 +1813,17 @@ namespace Barotrauma RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero }; } - //skillContainer.Recalculate(); + skillContainer.Recalculate(); } parent.RecalculateChildren(); GUITextBlock.AutoScaleAndNormalize(skillNames); } - private void UpdateTalentInfo() - { - Character controlledCharacter = Character.Controlled; - if (controlledCharacter?.Info == null) { return; } - - if (SelectedTab != InfoFrameTab.Talents) { return; } - - bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents(); - - if (unlockedAllTalents) - { - experienceText.Text = string.Empty; - experienceBar.BarSize = 1f; - } - else - { - experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; - experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel(); - } - - selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); - - string pointsLeft = controlledCharacter.Info.GetAvailableTalentPoints().ToString(); - - int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count(); - - if (unlockedAllTalents) - { - talentPointText.SetRichText($"‖color:{XMLExtensions.ToStringHex(Color.Gray)}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖"); - } - else if (talentCount > 0) - { - string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖"; - LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed)); - talentPointText.SetRichText(localizedString); - } - else - { - talentPointText.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft)); - } - - foreach (var (talentTree, index, icon, frame, glow) in talentCornerIcons) - { - TalentTree.TalentTreeStageState state = TalentTree.GetTalentOptionStageState(controlledCharacter, talentTree, index, selectedTalents); - GUIComponentStyle newStyle = talentStageStyles[state]; - icon.ApplyStyle(newStyle); - icon.Color = newStyle.Color; - frame.Color = talentStageBackgroundColors[state]; - glow.Visible = state == TalentTree.TalentTreeStageState.Highlighted; - } - - foreach (var talentButton in talentButtons) - { - Identifier talentIdentifier = (Identifier)talentButton.button.UserData; - bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); - Color newTalentColor = unselectable ? unselectableColor : unselectedColor; - Color hoverColor = Color.White; - - if (controlledCharacter.HasTalent(talentIdentifier)) - { - newTalentColor = GUIStyle.Green; - } - else if (selectedTalents.Contains(talentIdentifier)) - { - newTalentColor = GUIStyle.Orange; - hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f); - } - - talentButton.icon.Color = newTalentColor; - talentButton.icon.HoverColor = hoverColor; - } - - CreateSkillList(controlledCharacter, controlledCharacter.Info, skillListBox); - } - - private void ApplyTalents(Character controlledCharacter) - { - selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); - foreach (Identifier talent in selectedTalents) - { - controlledCharacter.GiveTalent(talent); - if (GameMain.Client != null) - { - GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData()); - } - } - selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); - UpdateTalentInfo(); - } - - private bool ApplyTalentSelection(GUIButton guiButton, object userData) - { - Character controlledCharacter = Character.Controlled; - ApplyTalents(controlledCharacter); - return true; - } - - private bool ResetTalentSelection(GUIButton guiButton, object userData) - { - Character controlledCharacter = Character.Controlled; - if (controlledCharacter?.Info == null) { return false; } - selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); - UpdateTalentInfo(); - return true; - } - public void OnExperienceChanged(Character character) { if (character != Character.Controlled) { return; } - UpdateTalentInfo(); + talentMenu.UpdateTalentInfo(); } public void OnClose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs new file mode 100644 index 000000000..354ef4180 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -0,0 +1,817 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using static Barotrauma.TalentTree; +using static Barotrauma.TalentTree.TalentStages; + +namespace Barotrauma +{ + internal readonly record struct TalentShowCaseButton(ImmutableHashSet Buttons, + GUIComponent IconComponent); + + internal readonly record struct TalentButton(GUIComponent IconComponent, + TalentPrefab Prefab) + { + public Identifier Identifier => Prefab.Identifier; + } + + internal readonly record struct TalentCornerIcon(Identifier TalentTree, + int Index, + GUIImage IconComponent, + GUIFrame BackgroundComponent, + GUIFrame GlowComponent); + + internal readonly struct TalentTreeStyle + { + public readonly GUIComponentStyle ComponentStyle; + public readonly Color Color; + + public TalentTreeStyle(string componentStyle, Color color) + { + ComponentStyle = GUIStyle.GetComponentStyle(componentStyle); + Color = color; + } + } + + internal sealed class TalentMenu + { + public const string ManageBotTalentsButtonUserData = "managebottalentsbutton"; + + private Character? character; + private CharacterInfo? characterInfo; + + private static readonly Color unselectedColor = new Color(240, 255, 255, 225), + unselectableColor = new Color(100, 100, 100, 225), + pressedColor = new Color(60, 60, 60, 225), + lockedColor = new Color(48, 48, 48, 255), + unlockedColor = new Color(24, 37, 31, 255), + availableColor = new Color(50, 47, 33, 255); + + private static readonly ImmutableDictionary talentStageStyles = + new Dictionary + { + [Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor), + [Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor), + [Unlocked] = new TalentTreeStyle("TalentTreePurchased", unlockedColor), + [Available] = new TalentTreeStyle("TalentTreeUnlocked", availableColor), + [Highlighted] = new TalentTreeStyle("TalentTreeAvailable", availableColor) + }.ToImmutableDictionary(); + + private readonly HashSet talentButtons = new HashSet(); + private readonly HashSet talentShowCaseButtons = new HashSet(); + private readonly HashSet showCaseTalentFrames = new HashSet(); + private readonly HashSet talentCornerIcons = new HashSet(); + private HashSet selectedTalents = new HashSet(); + + private readonly Queue showCaseClosureQueue = new(); + + private GUIListBox? skillListBox; + private GUITextBlock? talentPointText; + private GUIProgressBar? experienceBar; + private GUITextBlock? experienceText; + private GUILayoutGroup? skillLayout; + + private GUIButton? talentApplyButton, + talentResetButton; + + public void CreateGUI(GUIFrame parent, Character? targetCharacter) + { + parent.ClearChildren(); + talentButtons.Clear(); + talentShowCaseButtons.Clear(); + talentCornerIcons.Clear(); + showCaseTalentFrames.Clear(); + + character = targetCharacter; + characterInfo = targetCharacter?.Info; + + GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + int padding = GUI.IntScale(15); + GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null); + + GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null); + + GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = GUI.IntScale(10), + Stretch = true + }; + + if (characterInfo is null) { return; } + + CreateStatPanel(contentLayout, characterInfo); + + new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine"); + + if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree)) + { + CreateTalentMenu(contentLayout, characterInfo, talentTree!); + } + + CreateFooter(contentLayout, characterInfo); + UpdateTalentInfo(); + + if (GameMain.NetworkMember != null && IsOwnCharacter(characterInfo)) + { + CreateMultiplayerCharacterSettings(frame, content); + } + } + + private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content) + { + if (skillLayout is null) { return; } + + GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false }; + GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); + GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); + GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); + GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); + + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), + text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") + { + IgnoreLayoutGroups = false, + TextBlock = + { + AutoScaleHorizontal = true + } + }; + + newCharacterBox.OnClicked = (button, o) => + { + if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + { + newCharacterBox.Text = TextManager.Get("settings"); + if (TabMenu.PendingChangesFrame != null) + { + NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame); + } + + OpenMenu(); + }); + return true; + } + + OpenMenu(); + return true; + + void OpenMenu() + { + characterSettingsFrame!.Visible = true; + content.Visible = false; + } + }; + + GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); + new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? + { + OnClicked = (button, o) => + { + GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); + characterSettingsFrame.Visible = false; + content.Visible = true; + return true; + } + }; + } + + private void CreateStatPanel(GUIComponent parent, CharacterInfo info) + { + Job job = info.Job; + + GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform, Anchor.Center), isHorizontal: true); + + new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => + { + info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false); + }); + + GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) + { + AbsoluteSpacing = GUI.IntScale(5), + CanBeFocused = true + }; + + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); + + if (!info.OmitJobInMenus) + { + nameBlock.TextColor = job.Prefab.UIColor; + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; + } + + if (info.PersonalityTrait != null) + { + LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName); + Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); + GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); + traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); + } + + ImmutableHashSet talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet(); + if (talentsOutsideTree.Any()) + { + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null); + + GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); + + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont) + { + AutoScaleVertical = true + }; + talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y); + + var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) + { + AutoHideScrollBar = false, + ResizeContentToMakeSpaceForScrollBar = false + }; + extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter); + extraTalentLayout.Recalculate(); + extraTalentList.ForceLayoutRecalculation(); + + foreach (var extraTalent in talentsOutsideTree) + { + if (extraTalent is null) { continue; } + GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true) + { + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)), + Color = GUIStyle.Green + }; + } + } + + skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight) + { + AbsoluteSpacing = GUI.IntScale(5), + Stretch = true + }; + + GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont); + + skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); + TabMenu.CreateSkillList(info.Character, info, skillListBox); + } + + private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree) + { + GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter)); + + selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet(); + + var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization); + + List subTreeNames = new List(); + foreach (var subTree in tree.TalentSubTrees) + { + GUIListBox talentList; + GUIComponent talentParent; + Vector2 treeSize; + switch (subTree.Type) + { + case TalentTreeType.Primary: + talentList = mainList; + treeSize = new Vector2(1f, 0.5f); + break; + case TalentTreeType.Specialization: + talentList = GetSpecializationList(); + treeSize = new Vector2(Math.Max(0.333f, 1.0f / tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization)), 1f); + break; + default: + throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\""); + } + talentParent = talentList.Content; + + GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(treeSize, talentParent.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter) + { + Stretch = true + }; + + if (subTree.Type != TalentTreeType.Primary) + { + GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter) + { MinSize = new Point(0, GUI.IntScale(30)) }, style: null); + subtreeTitleFrame.RectTransform.IsFixedSize = true; + int elementPadding = GUI.IntScale(8); + Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; + GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); + } + + int optionAmount = subTree.TalentOptionStages.Length; + for (int i = 0; i < optionAmount; i++) + { + TalentOption option = subTree.TalentOptionStages[i]; + CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount); + } + subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width, + subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing))); + subTreeLayoutGroup.RectTransform.MinSize = new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Rect.Height); + subTreeLayoutGroup.Recalculate(); + + if (subTree.Type == TalentTreeType.Specialization) + { + talentList.RectTransform.Resize(new Point(talentList.Rect.Width, Math.Max(subTreeLayoutGroup.Rect.Height, talentList.Rect.Height))); + talentList.RectTransform.MinSize = new Point(0, talentList.Rect.Height); + } + } + + var specializationList = GetSpecializationList(); + //resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu + specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height), + resizeChildren: specializationCount < 3); + //make room for scrollbar if there's more than the default amount of specializations + if (specializationCount > 3) + { + specializationList.RectTransform.MinSize = new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (int)(specializationList.ScrollBar.Rect.Height * 0.9f)); + } + + GUITextBlock.AutoScaleAndNormalize(subTreeNames); + + GUIListBox GetSpecializationList() + { + if (mainList.Content.Children.LastOrDefault() is GUIListBox specList) + { + return specList; + } + GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + return newSpecializationList; + } + } + + private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount) + { + int elementPadding = GUI.IntScale(8); + int height = GUI.IntScale((GameMain.GameSession?.Campaign == null ? 65 : 60) * (specializationCount > 3 ? 0.97f : 1.0f)); + GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter) + { MinSize = new Point(0, height) }, style: null); + + Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + + GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), + style: "TalentBackground") + { + Color = talentStageStyles[Locked].Color + }; + GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; + + GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + { + CanBeFocused = false, + Color = talentStageStyles[Locked].Color + }; + + Point iconSize = cornerIcon.RectTransform.NonScaledSize; + cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + + GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.9f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); + GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + + HashSet talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet(); + HashSet buttonsToAdd = new(); + + Dictionary> showCaseTalentParents = new(); + Dictionary showCaseTalentButtonsToAdd = new(); + + foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents) + { + talentOptionIdentifiers.Add(showCaseTalentIdentifier); + Point parentSize = talentBackground.RectTransform.NonScaledSize; + GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talents.Count - 1)), parentSize.Y)), style: "GUITooltip") + { + UserData = showCaseTalentIdentifier, + IgnoreLayoutGroups = true, + Visible = false + }; + GUILayoutGroup showcaseCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.7f), showCaseFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); + GUILayoutGroup showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true }; + showCaseTalentParents.Add(showcaseLayout, talents); + showCaseTalentFrames.Add(showCaseFrame); + } + + foreach (Identifier talentId in talentOptionIdentifiers) + { + if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; } + + bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId); + GUIComponent talentParent = talentOptionLayoutGroup; + + foreach (var (key, value) in showCaseTalentParents) + { + if (value.Contains(talentId)) + { + talentParent = key; + break; + } + } + + GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null) + { + CanBeFocused = false + }; + + GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); + GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) + { + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)), + UserData = talent.Identifier, + PressedColor = pressedColor, + Enabled = info.Character != null, + OnClicked = (button, userData) => + { + if (isShowCaseTalent) + { + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId) + { + component.RectTransform.ScreenSpaceOffset = new Point((int)(button.Rect.Location.X - component.Rect.Width / 2f + button.Rect.Width / 2f), button.Rect.Location.Y - component.Rect.Height); + component.Visible = true; + } + else + { + component.Visible = false; + } + } + + return true; + } + + if (character is null) { return false; } + + Identifier talentIdentifier = (Identifier)userData; + if (talentOption.MaxChosenTalents is 1) + { + // deselect other buttons in tier by removing their selected talents from pool + foreach (Identifier identifier in selectedTalents) + { + if (character.HasTalent(identifier) || identifier == talentId) { continue; } + + if (talentOptionIdentifiers.Contains(identifier)) + { + selectedTalents.Remove(identifier); + } + } + } + + if (character.HasTalent(talentIdentifier)) + { + return true; + } + else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents)) + { + if (!selectedTalents.Contains(talentIdentifier)) + { + selectedTalents.Add(talentIdentifier); + } + else + { + selectedTalents.Remove(talentIdentifier); + } + } + else + { + selectedTalents.Remove(talentIdentifier); + } + + UpdateTalentInfo(); + return true; + }, + }; + + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; + + GUIComponent iconImage; + if (talent.Icon is null) + { + iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null) + { + OutlineColor = GUIStyle.Red, + TextColor = GUIStyle.Red, + PressedColor = unselectableColor, + DisabledColor = unselectableColor, + CanBeFocused = false, + }; + } + else + { + iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) + { + Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White, + PressedColor = unselectableColor, + DisabledColor = unselectableColor * 0.5f, + CanBeFocused = false, + }; + } + + iconImage.Enabled = talentButton.Enabled; + if (isShowCaseTalent) + { + showCaseTalentButtonsToAdd.Add(talentId, iconImage); + continue; + } + + buttonsToAdd.Add(new TalentButton(iconImage, talent)); + } + + foreach (TalentButton button in buttonsToAdd) + { + talentButtons.Add(button); + } + + foreach (var (key, value) in showCaseTalentButtonsToAdd) + { + HashSet buttons = new(); + foreach (Identifier identifier in talentOption.ShowCaseTalents[key]) + { + if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) { continue; } + + buttons.Add(button); + } + + talentShowCaseButtons.Add(new TalentShowCaseButton(buttons.ToImmutableHashSet(), value)); + } + + talentCornerIcons.Add(new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight)); + } + + private void CreateFooter(GUIComponent parent, CharacterInfo info) + { + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), parent.RectTransform, Anchor.TopCenter), isHorizontal: true) + { + RelativeSpacing = 0.01f, + Stretch = true + }; + + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform)); + GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); + + experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), + barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) + { + IsHorizontal = true, + }; + + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) + { + Shadow = true, + ToolTip = TextManager.Get("experiencetooltip") + }; + + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) + { AutoScaleVertical = true }; + + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + { + OnClicked = ResetTalentSelection + }; + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") + { + OnClicked = ApplyTalentSelection, + }; + GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + } + + private bool ResetTalentSelection(GUIButton guiButton, object userData) + { + if (characterInfo is null) { return false; } + selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet(); + UpdateTalentInfo(); + return true; + } + + private void ApplyTalents(Character controlledCharacter) + { + foreach (Identifier talent in CheckTalentSelection(controlledCharacter, selectedTalents)) + { + controlledCharacter.GiveTalent(talent); + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData()); + } + } + + UpdateTalentInfo(); + } + + private bool ApplyTalentSelection(GUIButton guiButton, object userData) + { + if (character is null) { return false; } + + ApplyTalents(character); + return true; + } + + public void UpdateTalentInfo() + { + if (character is null || characterInfo is null) { return; } + + bool unlockedAllTalents = character.HasUnlockedAllTalents(); + + if (experienceBar is null || experienceText is null) { return; } + + if (unlockedAllTalents) + { + experienceText.Text = string.Empty; + experienceBar.BarSize = 1f; + } + else + { + experienceText.Text = $"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}"; + experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel(); + } + + selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet(); + + string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString(); + + int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); + + if (unlockedAllTalents) + { + talentPointText?.SetRichText($"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖"); + } + else if (talentCount > 0) + { + string pointsUsed = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{-talentCount}‖color:end‖"; + LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed)); + talentPointText?.SetRichText(localizedString); + } + else + { + talentPointText?.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft)); + } + + foreach (TalentCornerIcon cornerIcon in talentCornerIcons) + { + TalentStages state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents); + TalentTreeStyle style = talentStageStyles[state]; + GUIComponentStyle newStyle = style.ComponentStyle; + cornerIcon.IconComponent.ApplyStyle(newStyle); + cornerIcon.IconComponent.Color = newStyle.Color; + cornerIcon.BackgroundComponent.Color = style.Color; + cornerIcon.GlowComponent.Visible = state == Highlighted; + } + + foreach (TalentButton talentButton in talentButtons) + { + TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents); + ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride); + } + + foreach (TalentShowCaseButton showCaseTalentButton in talentShowCaseButtons) + { + TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents); + ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option.None()); + } + + if (skillListBox is null) { return; } + + TabMenu.CreateSkillList(character, characterInfo, skillListBox); + + static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection selectedTalents) + { + bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier); + TalentStages stage = unselectable ? Locked : Available; + if (unselectable) + { + stage = Locked; + } + + if (character.HasTalent(talentIdentifier)) + { + stage = Unlocked; + } + else if (selectedTalents.Contains(talentIdentifier)) + { + stage = Highlighted; + } + + return stage; + } + + static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option colorOverride) + { + Color color = stage switch + { + Invalid => unselectableColor, + Locked => unselectableColor, + Unlocked => GetColorOrOverride(GUIStyle.Green, colorOverride), + Highlighted => GetColorOrOverride(GUIStyle.Orange, colorOverride), + Available => GetColorOrOverride(unselectedColor, colorOverride), + _ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null) + }; + + component.Color = color; + component.HoverColor = Color.Lerp(color, Color.White, 0.7f); + + static Color GetColorOrOverride(Color color, Option colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color; + } + + // this could also be reused for setting colors for talentCornerIcons but that's for another time + static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection buttons, IReadOnlyCollection selectedTalents) + { + HashSet talentStages = new HashSet(); + foreach (TalentButton button in buttons) + { + talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents)); + } + + TalentStages collectiveStage = talentStages.Any(static stage => stage is Locked) + ? Locked + : Available; + + foreach (TalentStages stage in talentStages) + { + if (stage is Highlighted) + { + collectiveStage = Highlighted; + break; + } + + if (stage is Unlocked) + { + collectiveStage = Unlocked; + break; + } + } + + return collectiveStage; + } + } + + public void Update() + { + if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; } + + int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); + talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; + if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) + { + talentApplyButton.Flash(GUIStyle.Orange); + } + + while (showCaseClosureQueue.TryDequeue(out Identifier identifier)) + { + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier) + { + component.Visible = false; + } + } + } + + bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0; + bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit(); + + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is not Identifier identifier) { continue; } + + component.AddToGUIUpdateList(order: 1); + if (!component.Visible) { continue; } + + if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition))) + { + showCaseClosureQueue.Enqueue(identifier); + } + } + } + + private static bool IsOwnCharacter(CharacterInfo? info) + { + if (info is null) { return false; } + + CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo; + if (ownCharacterInfo is null) { return false; } + + return info == ownCharacterInfo; + } + + public static bool CanManageTalents(CharacterInfo targetInfo) + { + // in singleplayer we can do whatever we want + if (GameMain.IsSingleplayer) { return true; } + + // always allow managing talents for own character + if (IsOwnCharacter(targetInfo)) { return true; } + + // don't allow controlling non-bot characters + if (targetInfo.Character is not { IsBot: true }) { return false; } + + // lastly check if we have the permission to do this + return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 0a78a8663..445a77787 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -278,10 +278,13 @@ namespace Barotrauma * | upgrades | maintenance | <- 1/3rd empty space | * |---------------------------------------------------------------------------------------------------| */ - GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; + GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.4f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true); GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true); - new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); + var header = new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); + header.RectTransform.MaxSize = new Point((int)(header.TextSize.X + header.Padding.X + header.Padding.Z), int.MaxValue); + new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true); + categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; @@ -433,8 +436,8 @@ namespace Barotrauma Location location = Campaign.Map.CurrentLocation; - int hullRepairCost = Campaign.GetHullRepairCost(); - int itemRepairCost = Campaign.GetItemRepairCost(); + int hullRepairCost = CampaignMode.GetHullRepairCost(); + int itemRepairCost = CampaignMode.GetItemRepairCost(); int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost; if (location != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index e8069b34b..a0a55d717 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -30,7 +30,7 @@ namespace Barotrauma Data = data, OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) => { - GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); + GameMain.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 491e77a4b..53966b04a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Steam; using Barotrauma.Transition; +using Barotrauma.Tutorials; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -15,6 +16,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; +using Barotrauma.Extensions; namespace Barotrauma { @@ -317,7 +319,7 @@ namespace Barotrauma GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync; SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode); - defaultViewport = GraphicsDevice.Viewport; + defaultViewport = new Viewport(0, 0, GraphicsWidth, GraphicsHeight); if (recalculateFontsAndStyles) { @@ -356,6 +358,7 @@ namespace Barotrauma public void ResetViewPort() { GraphicsDevice.Viewport = defaultViewport; + GraphicsDevice.ScissorRectangle = defaultViewport.Bounds; } /// @@ -378,6 +381,7 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); performanceCounterTimer = Stopwatch.StartNew(); + ResetIMEWorkaround(); } /// @@ -467,10 +471,11 @@ namespace Barotrauma LegacySteamUgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); - foreach (var progress in contentPackageLoadRoutine) + foreach (var progress in contentPackageLoadRoutine + .Select(p => p.Result).Successes()) { const float min = 1f, max = 70f; - TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value); + TitleScreen.LoadState = MathHelper.Lerp(min, max, progress); yield return CoroutineStatus.Running; } @@ -783,9 +788,9 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) + else if (ObjectiveManager.ContentRunning) { - tutorialMode.Tutorial.CloseActiveContentGUI(); + ObjectiveManager.CloseActiveContentGUI(); } else if (GameSession.IsTabMenuOpen) { @@ -800,6 +805,10 @@ namespace Barotrauma { GUI.TogglePauseMenu(); } + else if (GameSession?.Campaign is { ShowCampaignUI: true, ForceMapUI: false }) + { + GameSession.Campaign.ShowCampaignUI = false; + } //open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC else if ((Character.Controlled == null || !itemHudActive()) && CharacterHealth.OpenHealthWindow == null @@ -833,7 +842,7 @@ namespace Barotrauma Paused = (DebugConsole.IsOpen || DebugConsole.Paused || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || - (GameSession?.GameMode is TutorialMode tutoMode && tutoMode.Tutorial.ContentRunning)) && + (GameSession?.GameMode is TutorialMode && ObjectiveManager.ContentRunning)) && (NetworkMember == null || !NetworkMember.GameStarted); if (GameSession?.GameMode != null && GameSession.GameMode.Paused) { @@ -867,8 +876,9 @@ namespace Barotrauma { Screen.Selected.Update(Timing.Step); } - else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) + else if (ObjectiveManager.ContentRunning && GameSession?.GameMode is TutorialMode tutorialMode) { + ObjectiveManager.VideoPlayer.Update(); tutorialMode.Update((float)Timing.Step); } else @@ -1070,11 +1080,9 @@ namespace Barotrauma } // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) + if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) { - spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems); - spCampaign.CargoManager.ClearSoldItemsProjSpecific(); - spCampaign.Map.CurrentLocation.RemoveStock(spCampaign.CargoManager.PurchasedItems); + spCampaign.UpdateStoreStock(); } SaveUtil.SaveGame(GameSession.SavePath); @@ -1090,10 +1098,9 @@ namespace Barotrauma if (GameSession != null) { - double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, GameSession.GameMode?.Preset.Identifier.Value ?? "none", - roundDuration); + GameSession.RoundDuration); string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); foreach (var activeEvent in GameSession.EventManager.ActiveEvents) @@ -1221,7 +1228,7 @@ namespace Barotrauma base.OnExiting(sender, args); } - public void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) + public static void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) { if (string.IsNullOrEmpty(url)) { return; } if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } @@ -1239,11 +1246,33 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked = (btn, userdata) => { - ToolBox.OpenFileWithShell(url); + try + { + ToolBox.OpenFileWithShell(url); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to open the url {url}", e); + } msgBox.Close(); return true; }; msgBox.Buttons[1].OnClicked = msgBox.Close; } + + /* + * On some systems, IME input is enabled by default, and being able to set the game to a state + * where it doesn't accept IME input on game launch seems very inconsistent. + * This function quickly cycles through IME input states and is called from couple different places + * to ensure that IME input is disabled properly when it's not needed. + */ + public static void ResetIMEWorkaround() + { + Rectangle rect = new Rectangle(0, 0, GraphicsWidth, GraphicsHeight); + TextInput.SetTextInputRect(rect); + TextInput.StartTextInput(); + TextInput.SetTextInputRect(rect); + TextInput.StopTextInput(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index dd4ca114b..baf76b999 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -11,7 +11,15 @@ namespace Barotrauma private List SoldEntities { get; } = new List(); // The bag slot is intentionally left out since we want to be able to sell items from there - private readonly List equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; + private static readonly HashSet equipmentSlots = new HashSet() + { + InvSlotType.Head, + InvSlotType.InnerClothes, + InvSlotType.OuterClothes, + InvSlotType.Headset, + InvSlotType.Card, + InvSlotType.HealthInterface + }; public IEnumerable GetSellableItems(Character character) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 19a52394e..856417e04 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -86,6 +87,8 @@ namespace Barotrauma } } + private static bool IsOwner(Client client) => client != null && client.IsOwner; + /// /// There is a server-side implementation of the method in /// @@ -97,10 +100,8 @@ namespace Barotrauma return GameMain.Client.HasPermission(permissions) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Client.ConnectedClients.Count == 1 || GameMain.Client.IsServerOwner || - //allow managing if no-one with permissions is alive - GameMain.Client.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (c.IsOwner || c.HasPermission(permissions))); + AnyOneAllowedToManageCampaign(permissions); } public static bool AllowedToManageWallets() @@ -203,6 +204,10 @@ namespace Barotrauma } break; } + if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) + { + endRoundButton.Visible = false; + } if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } @@ -266,7 +271,7 @@ namespace Barotrauma Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; try { - GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); + GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); } catch (Exception e) { @@ -283,6 +288,18 @@ namespace Barotrauma return loadTask; } + protected SubmarineInfo GetPredefinedStartOutpost() + { + if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) + { + return new SubmarineInfo(parameters.OutpostFilePath.Value) + { + OutpostGenerationParams = parameters + }; + } + return null; + } + partial void NPCInteractProjSpecific(Character npc, Character interactor) { if (npc == null || interactor == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 94ba1b79a..470e9be68 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -694,8 +694,18 @@ namespace Barotrauma if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false)) { - foreach (int ownedSubIndex in ownedSubIndices) + foreach (ushort ownedSubIndex in ownedSubIndices) { + if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count) + { + string errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + continue; + } + SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index dc7bc12b1..722297b66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -265,8 +265,8 @@ namespace Barotrauma private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { - GameMain.GameSession.StartRound(level, - mirrorLevel: mirror); + + GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "SinglePlayerCampaign.DoInitialCameraTransition"); @@ -407,6 +407,11 @@ namespace Barotrauma GUI.SetSavingIndicatorState(success); crewDead = false; + if (success) + { + // Event history must be registered before ending the round or it will be cleared + GameMain.GameSession.EventManager.RegisterEventHistory(); + } GameMain.GameSession.EndRound("", traitorResults, transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; @@ -439,7 +444,7 @@ namespace Barotrauma break; } - Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, @@ -466,7 +471,6 @@ namespace Barotrauma if (success) { GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - GameMain.GameSession.EventManager.RegisterEventHistory(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index a9c7f14e8..d6c8f225d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -1,7 +1,5 @@ using Barotrauma.Extensions; -using Barotrauma.IO; using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -25,107 +23,9 @@ namespace Barotrauma.Tutorials #region Tutorial variables public readonly Identifier Identifier; - public LocalizedString DisplayName { get; } - - public bool ContentRunning { get; private set; } - - private GUIComponent infoBox; - private Action infoBoxClosedCallback; - - private VideoPlayer videoPlayer; - private Point screenResolution; - private WindowMode windowMode; - private float prevUIScale; - - private GUILayoutGroup objectiveGroup; - private readonly LocalizedString objectiveTextTranslated; - - private readonly List ActiveObjectives = new List(); - private const float ObjectiveComponentAnimationTime = 1.5f; - private Segment ActiveContentSegment { get; set; } - - public class Segment - { - public readonly record struct Text( - Identifier Tag, - int Width = DefaultWidth, - int Height = DefaultHeight, - Anchor Anchor = Anchor.Center); - - public readonly record struct Video( - string FullPath, - Identifier TextTag, - int Width = DefaultWidth, - int Height = DefaultHeight) - { - public string FileName => Path.GetFileName(FullPath.CleanUpPath()); - public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); - } - - private const int DefaultWidth = 450; - private const int DefaultHeight = 80; - - public GUIImage ObjectiveStateIndicator; - public GUIButton ObjectiveButton; - public GUITextBlock LinkedTextBlock; - public LocalizedString ObjectiveText; - - public readonly Identifier Id; - public readonly Text TextContent; - public readonly Video VideoContent; - public readonly AutoPlayVideo AutoPlayVideo; - - public Action OnClickObjective; - - public TutorialSegmentType SegmentType { get; private set; } - - public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) - { - return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); - } - - public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) - { - return new Segment(id, objectiveTextTag, onClickObjective); - } - - public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) - { - return new Segment(id, objectiveTextTag); - } - - private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - AutoPlayVideo = autoPlayVideo; - TextContent = textContent; - VideoContent = videoContent; - SegmentType = TutorialSegmentType.InfoBox; - } - - private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - OnClickObjective = onClickObjective; - SegmentType = TutorialSegmentType.MessageBox; - } - - private Segment(Identifier id, Identifier objectiveTextTag) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - SegmentType = TutorialSegmentType.Objective; - } - - public void ConnectMessageBox(Segment messageBoxSegment) - { - SegmentType = TutorialSegmentType.MessageBox; - OnClickObjective = messageBoxSegment.OnClickObjective; - } - } + public LocalizedString Description { get; } + private bool completed; public bool Completed @@ -163,6 +63,8 @@ namespace Barotrauma.Tutorials public readonly List<(Entity entity, Identifier iconStyle)> Icons = new List<(Entity entity, Identifier iconStyle)>(); + public bool Paused { get; private set; } + #endregion #region Tutorial Controls @@ -171,8 +73,7 @@ namespace Barotrauma.Tutorials { Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier(); DisplayName = TextManager.Get(Identifier); - objectiveTextTranslated = TextManager.Get("Tutorial.Objective"); - + Description = TextManager.Get($"tutorial.{prefab.Identifier}.description"); TutorialPrefab = prefab; eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier); } @@ -260,35 +161,26 @@ namespace Barotrauma.Tutorials tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); - Initialize(); + GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; + GameMain.GameSession.CrewManager.AutoHideCrewList(); + + if (Character.Controlled?.Inventory is CharacterInventory inventory) + { + foreach (Item item in inventory.AllItemsMod) + { + if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } + item.Unequip(Character.Controlled); + Character.Controlled.Inventory.RemoveItem(item); + } + } yield return CoroutineStatus.Success; } - private void Initialize() - { - GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; - GameMain.GameSession.CrewManager.AutoHideCrewList(); - - if (Character.Controlled is Character character) - { - foreach (Item item in character.Inventory.AllItemsMod) - { - if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } - item.Unequip(character); - character.Inventory.RemoveItem(item); - } - } - } - public void Start() { - videoPlayer = new VideoPlayer(); GameMain.Instance.ShowLoading(Loading()); - ActiveObjectives.Clear(); - ActiveContentSegment = null; - - CreateObjectiveFrame(); + ObjectiveManager.ResetObjectives(); // Setup doors: Clear all requirements, unless the door is setup as locked. foreach (var item in Item.ItemList) @@ -304,24 +196,8 @@ namespace Barotrauma.Tutorials } } - public void AddToGUIUpdateList() - { - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode) - { - CreateObjectiveFrame(); - } - if (ActiveObjectives.Count > 0) - { - objectiveGroup?.AddToGUIUpdateList(order: -1); - } - infoBox?.AddToGUIUpdateList(order: 100); - videoPlayer?.AddToGUIUpdateList(order: 100); - } - public void Update() { - videoPlayer?.Update(); - if (character != null) { if (character.Oxygen < 1) @@ -342,8 +218,7 @@ namespace Barotrauma.Tutorials { GUI.PreventPauseMenuToggle = false; } - ContentRunning = false; - infoBox = null; + ObjectiveManager.ClearContent(); } else { @@ -374,18 +249,6 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Success; } - public void CloseActiveContentGUI() - { - if (videoPlayer.IsPlaying) - { - videoPlayer.Stop(); - } - else if (infoBox != null) - { - CloseInfoFrame(); - } - } - public IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen || Level.Loaded == null || Level.Loaded.Generating) @@ -432,13 +295,56 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(WaitBeforeFade); + Action onEnd = () => GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + + TutorialPrefab nextTutorialPrefab = null; + bool displayEndMessage = + TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Restart || + (TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Continue && TutorialPrefab.Prefabs.TryGet(TutorialPrefab.EndMessage.NextTutorialIdentifier, out nextTutorialPrefab)); + + if (displayEndMessage) + { + Paused = true; + var endingMessageBox = new GUIMessageBox( + headerText: "", + text: TextManager.Get($"{Identifier}.completed"), + buttons: new LocalizedString[] + { + TextManager.Get(nextTutorialPrefab is null ? "restart" : "campaigncontinue"), + TextManager.Get("pausemenuquit") + }); + + endingMessageBox.Buttons[0].OnClicked += (_, _) => + { + if (nextTutorialPrefab is null) + { + onEnd = () => Restart(null, null); + } + else + { + onEnd = () => + { + GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + new Tutorial(nextTutorialPrefab).Start(); + }; + } + return true; + }; + endingMessageBox.Buttons[0].OnClicked += endingMessageBox.Close; + endingMessageBox.Buttons[0].OnClicked += (_, _) => Paused = false; + endingMessageBox.Buttons[1].OnClicked += endingMessageBox.Close; + endingMessageBox.Buttons[1].OnClicked += (_, _) => Paused = false; + } + + while (Paused) { yield return CoroutineStatus.Running; } + var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: FadeOutTime); Completed = true; while (endCinematic.Running) { yield return CoroutineStatus.Running; } Stop(); - GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + onEnd(); } } @@ -450,379 +356,15 @@ namespace Barotrauma.Tutorials return true; } - public void TriggerTutorialSegment(Segment segment, bool connectObjective = false) - { - if (segment.SegmentType != TutorialSegmentType.InfoBox) - { - ActiveObjectives.Add(segment); - AddToObjectiveList(segment, connectObjective); - return; - } - - Inventory.DraggingItems.Clear(); - ContentRunning = true; - ActiveContentSegment = segment; - - var title = TextManager.Get(segment.Id); - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); - tutorialText = TextManager.ParseInputTypes(tutorialText); - - switch (segment.AutoPlayVideo) - { - case AutoPlayVideo.Yes: - infoBox = CreateInfoFrame( - title, - tutorialText, - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: LoadActiveContentVideo); - break; - case AutoPlayVideo.No: - infoBox = CreateInfoFrame( - title, - tutorialText, - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: StopCurrentContentSegment, - onVideoButtonClicked: LoadActiveContentVideo); - break; - } - } - - public void CompleteTutorialSegment(Identifier segmentId) - { - if (GetActiveObjective(segmentId) is not Segment segment) - { - DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); - return; - } - if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) - { - //return if already completed - if (segment.ObjectiveStateIndicator.Style == style) { return; } - segment.ObjectiveStateIndicator.ApplyStyle(style); - } - segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); - segment.ObjectiveButton.OnClicked = null; - segment.ObjectiveButton.CanBeFocused = false; - GameAnalyticsManager.AddDesignEvent($"Tutorial:{Identifier}:{segmentId}:Completed"); - } - - public void RemoveTutorialSegment(Identifier segmentId) - { - if (GetActiveObjective(segmentId) is not Segment segment) - { - DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); - return; - } - segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); - segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); - var parent = segment.LinkedTextBlock.Parent; - parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => - { - ActiveObjectives.Remove(segment); - objectiveGroup?.Recalculate(); - }); - parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); - segment.ObjectiveButton.OnClicked = null; - segment.ObjectiveButton.CanBeFocused = false; - } - - private Segment GetActiveObjective(Identifier id) => ActiveObjectives.FirstOrDefault(s => s.Id == id); - public void Stop() { if (tutorialCoroutine != null) { CoroutineManager.StopCoroutines(tutorialCoroutine); } - ContentRunning = false; - infoBox = null; - videoPlayer?.Remove(); + ObjectiveManager.ResetUI(); } #endregion - - #region Objectives - - /// - /// Create the objective list that holds the objectives (called on start and on resolution change) - /// - private void CreateObjectiveFrame() - { - var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null); - objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) - { - AbsoluteSpacing = (int)GUIStyle.Font.LineHeight - }; - for (int i = 0; i < ActiveObjectives.Count; i++) - { - AddToObjectiveList(ActiveObjectives[i]); - } - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode; - prevUIScale = GUI.Scale; - } - - /// - /// Stops content running and adds the active segment to the objective list - /// - private void StopCurrentContentSegment() - { - if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) - { - ActiveObjectives.Add(ActiveContentSegment); - AddToObjectiveList(ActiveContentSegment); - } - ContentRunning = false; - ActiveContentSegment = null; - } - - /// - /// Adds the segment to the objective list - /// - private void AddToObjectiveList(Segment segment, bool connectExisting = false) - { - if (connectExisting) - { - if (ActiveObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) - { - existingSegment.ConnectMessageBox(segment); - SetButtonBehavior(existingSegment); - } - return; - } - - var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) - { - AbsoluteOffset = GetObjectiveHiddenPosition(), - MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) - }; - var frame = new GUIFrame(frameRt, style: null) - { - CanBeFocused = true - }; - objectiveGroup.Recalculate(); - - segment.LinkedTextBlock = new GUITextBlock( - new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight), - TextManager.ParseInputTypes(segment.ObjectiveText), - wrap: true); - - var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); - segment.LinkedTextBlock.RectTransform.NonScaledSize = size; - segment.LinkedTextBlock.RectTransform.MinSize = size; - segment.LinkedTextBlock.RectTransform.MaxSize = size; - segment.LinkedTextBlock.RectTransform.IsFixedSize = true; - frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); - frame.RectTransform.IsFixedSize = true; - - var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); - segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); - - SetTransparent(segment.LinkedTextBlock); - - segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) - { - ToolTip = objectiveTextTranslated - }; - SetButtonBehavior(segment); - SetTransparent(segment.ObjectiveButton); - - frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); - - static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; - - void SetButtonBehavior(Segment segment) - { - segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; - segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => - { - if (segment.SegmentType == TutorialSegmentType.InfoBox) - { - if (segment.AutoPlayVideo == AutoPlayVideo.Yes) - { - ReplaySegmentVideo(segment); - } - else - { - ShowSegmentText(segment); - } - } - else if (segment.SegmentType == TutorialSegmentType.MessageBox) - { - segment.OnClickObjective?.Invoke(); - } - return true; - }; - } - } - - private void ReplaySegmentVideo(Segment segment) - { - if (ContentRunning) { return; } - Inventory.DraggingItems.Clear(); - ContentRunning = true; - LoadVideo(segment); - } - - private void ShowSegmentText(Segment segment) - { - if (ContentRunning) { return; } - Inventory.DraggingItems.Clear(); - ContentRunning = true; - ActiveContentSegment = segment; - infoBox = CreateInfoFrame( - TextManager.Get(segment.Id), - TextManager.Get(segment.TextContent.Tag), - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: () => ContentRunning = false, - onVideoButtonClicked: () => LoadVideo(segment)); - } - - private Point GetObjectiveHiddenPosition(RectTransform rt = null) - { - return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); - } - - #endregion - - #region InfoFrame - - private void CloseInfoFrame() => CloseInfoFrame(null, null); - - private bool CloseInfoFrame(GUIButton button, object userData) - { - infoBox = null; - infoBoxClosedCallback?.Invoke(); - return true; - } - - /// - // Creates and displays a tutorial info box - /// - private GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) - { - if (hasButton) - { - height += 60; - } - - width = (int)(width * GUI.Scale); - height = (int)(height * GUI.Scale); - - LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); - height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; - - if (title.Length > 0) - { - height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); - } - - var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - - var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); - infoBlock.Flash(GUIStyle.Green); - - var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = 5 - }; - - if (title.Length > 0) - { - var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), - title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); - titleBlock.RectTransform.IsFixedSize = true; - } - - text = RichString.Rich(text); - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); - - textBlock.RectTransform.IsFixedSize = true; - infoBoxClosedCallback = onInfoBoxClosed; - - if (hasButton) - { - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.1f - }; - buttonContainer.RectTransform.IsFixedSize = true; - - if (onVideoButtonClicked != null) - { - buttonContainer.Stretch = true; - var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), - TextManager.Get("Video"), style: "GUIButtonLarge") - { - OnClicked = (GUIButton button, object obj) => - { - onVideoButtonClicked(); - return true; - } - }; - } - else - { - buttonContainer.Stretch = false; - buttonContainer.ChildAnchor = Anchor.Center; - } - - var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), - TextManager.Get("OK"), style: "GUIButtonLarge") - { - OnClicked = CloseInfoFrame - }; - } - - infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); - - SoundPlayer.PlayUISound(GUISoundType.UIMessage); - - return background; - } - - #endregion - - #region Video - - private void LoadVideo(Segment segment) - { - videoPlayer ??= new VideoPlayer(); - if (segment.AutoPlayVideo == AutoPlayVideo.Yes) - { - videoPlayer.LoadContent( - contentPath: segment.VideoContent.ContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), - textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), - contentId: segment.Id, - startPlayback: true, - objective: segment.ObjectiveText, - onStop: StopCurrentContentSegment); - } - else - { - videoPlayer.LoadContent( - contentPath: segment.VideoContent.ContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), - textSettings: null, - contentId: segment.Id, - startPlayback: true, - objective: string.Empty); - } - } - - private void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); - - #endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index d155a1c23..3a8b4a7f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public Tutorial Tutorial; + public override bool Paused => Tutorial.Paused; + public TutorialMode(GameModePreset preset) : base(preset) { } public override void Start() @@ -19,12 +21,6 @@ namespace Barotrauma } } - public override void AddToGUIUpdateList() - { - base.AddToGUIUpdateList(); - Tutorial.AddToGUIUpdateList(); - } - public override void Update(float deltaTime) { base.Update(deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index dd566ee34..90226c5e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -128,8 +129,9 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } GameMode?.AddToGUIUpdateList(); tabMenu?.AddToGUIUpdateList(); + ObjectiveManager.AddToGUIUpdateList(); - if ((!(GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && + if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { if (topLeftButtonGroup == null) @@ -223,6 +225,7 @@ namespace Barotrauma } HintManager.Update(); + ObjectiveManager.VideoPlayer.Update(); } public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 0dfc29ed7..fcb5438dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Tutorials; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -209,7 +210,7 @@ namespace Barotrauma { if (item.CurrentHull == null) { continue; } if (item.GetComponent() == null) { continue; } - if (!item.HasTag("ballast")) { continue; } + if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } BallastHulls.Add(item.CurrentHull); } } @@ -383,6 +384,34 @@ namespace Barotrauma IgnoreReminder("tabmenu"); } + public static void OnObtainedItem(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || item == null) { return; } + + if (DisplayHint($"onobtaineditem.{item.Prefab.Identifier}".ToIdentifier())) { return; } + foreach (Identifier tag in item.GetTags()) + { + if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; } + } + + if ((item.HasTag("geneticmaterial") && character.Inventory.FindItemByTag("geneticdevice".ToIdentifier(), recursive: true) != null) || + (item.HasTag("geneticdevice") && character.Inventory.FindItemByTag("geneticmaterial".ToIdentifier(), recursive: true) != null)) + { + if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; } + } + } + + public static void OnStartDeconstructing(Character character, Deconstructor deconstructor) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || deconstructor == null) { return; } + if (deconstructor.InputContainer.Inventory.AllItems.All(it => it.GetComponent() is not null)) + { + DisplayHint($"geneticmaterial.onrefiningorcombining".ToIdentifier()); + } + } + public static void OnStoleItem(Character character, Item item) { if (!CanDisplayHints()) { return; } @@ -507,7 +536,7 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } // Could make this more generic if there will ever be any other status effect related hints - if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } + if (component is not Repairable || actionType != ActionType.OnFailure) { return; } DisplayHint("onrepairfailed".ToIdentifier()); } @@ -563,7 +592,7 @@ namespace Barotrauma foreach (var me in gap.linkedTo) { if (me == Character.Controlled.CurrentHull) { continue; } - if (!(me is Hull adjacentHull)) { continue; } + if (me is not Hull adjacentHull) { continue; } if (!IsOnFriendlySub()) { continue; } if (IsWearingDivingSuit()) { continue; } if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; } @@ -720,6 +749,7 @@ namespace Barotrauma if (requireControllingCharacter && Character.Controlled == null) { return false; } var gameMode = GameMain.GameSession?.GameMode; if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; } + if (ObjectiveManager.AnyObjectives) { return false; } if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index c06efe8d5..232e84838 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -9,7 +9,7 @@ using Barotrauma.Networking; namespace Barotrauma { - internal partial class MedicalClinic + internal sealed partial class MedicalClinic { public enum RequestResult { @@ -19,63 +19,11 @@ namespace Barotrauma Timeout } - public readonly struct RequestAction - { - public readonly Action Callback; - public readonly DateTimeOffset Timeout; - - public RequestAction(Action callback, DateTimeOffset timeout) - { - Callback = callback; - Timeout = timeout; - } - } - - public readonly struct AfflictionRequest - { - public readonly RequestResult Result; - public readonly ImmutableArray Afflictions; - - public AfflictionRequest(RequestResult result, ImmutableArray afflictions) - { - Result = result; - Afflictions = afflictions; - } - } - - public readonly struct PendingRequest - { - public readonly RequestResult Result; - public readonly ImmutableArray CrewMembers; - - public PendingRequest(RequestResult result, ImmutableArray crewMembers) - { - Result = result; - CrewMembers = crewMembers; - } - } - - public readonly struct CallbackOnlyRequest - { - public readonly RequestResult Result; - - public CallbackOnlyRequest(RequestResult result) - { - Result = result; - } - } - - public readonly struct HealRequest - { - public readonly RequestResult Result; - public readonly HealRequestResult HealResult; - - public HealRequest(RequestResult result, HealRequestResult healResult) - { - Result = result; - HealResult = healResult; - } - } + public readonly record struct RequestAction(Action Callback, DateTimeOffset Timeout); + public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray Afflictions); + public readonly record struct PendingRequest(RequestResult Result, NetCollection CrewMembers); + public readonly record struct CallbackOnlyRequest(RequestResult Result); + public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult); private readonly List> afflictionRequests = new List>(); private readonly List> pendingHealRequests = new List>(); @@ -96,7 +44,7 @@ namespace Barotrauma } #endif - if (!(info is { Character: { CharacterHealth: { } health } })) + if (info is not { Character.CharacterHealth: { } health }) { onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); return; @@ -123,14 +71,14 @@ namespace Barotrauma public void Update(float deltaTime) { DateTimeOffset now = DateTimeOffset.Now; - UpdateQueue(afflictionRequests, now, onTimeout: callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); - UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); - UpdateQueue(healAllRequests, now, onTimeout: callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); + UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection.Empty)); }); + UpdateQueue(healAllRequests, now, onTimeout: static callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); - void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } + static void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } } public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction) @@ -148,9 +96,9 @@ namespace Barotrauma private static bool TryDequeue(List> requestQueue, out Action result) { RequestAction? first = requestQueue.FirstOrNull(); - if (!(first is { } action)) + if (first is not { } action) { - result = _ => { }; + result = static _ => { }; return false; } @@ -191,11 +139,25 @@ namespace Barotrauma private static int GetPing() { - if (GameMain.IsSingleplayer || !(GameMain.Client?.Name is { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; } + if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) { return 0; } return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } + public void TreatAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + AddEverythingToPending(); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + } + public void HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) @@ -296,8 +258,11 @@ namespace Barotrauma private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) { - NetCrewMember crewMember = INetSerializableStruct.Read(inc); - InsertPendingCrewMember(crewMember); + var crewMembers = INetSerializableStruct.Read>(inc); + foreach (var crewMember in crewMembers) + { + InsertPendingCrewMember(crewMember); + } if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback)) { callback(new CallbackOnlyRequest(RequestResult.Success)); @@ -318,11 +283,7 @@ namespace Barotrauma private static void SendAfflictionRequest(CharacterInfo info) { - INetSerializableStruct crewMember = new NetCrewMember - { - CharacterInfo = info, - Afflictions = Array.Empty() - }; + INetSerializableStruct crewMember = new NetCrewMember(info); ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable); } @@ -337,17 +298,17 @@ namespace Barotrauma NetCrewMember crewMember = INetSerializableStruct.Read(inc); if (TryDequeue(afflictionRequests, out var callback)) { - RequestResult result = crewMember.CharacterInfoID == 0 ? RequestResult.Error : RequestResult.Success; + RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } private void PendingRequestReceived(IReadMessage inc) { - NetPendingCrew pendingCrew = INetSerializableStruct.Read(inc); + var pendingCrew = INetSerializableStruct.Read>(inc); if (TryDequeue(pendingHealRequests, out var callback)) { - callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray())); + callback(new PendingRequest(RequestResult.Success, pendingCrew)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs new file mode 100644 index 000000000..ad65dcb8f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -0,0 +1,599 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +static class ObjectiveManager +{ + public class Segment + { + public readonly record struct Text( + Identifier Tag, + int Width = DefaultWidth, + int Height = DefaultHeight, + Anchor Anchor = Anchor.Center); + + public readonly record struct Video( + string FullPath, + Identifier TextTag, + int Width = DefaultWidth, + int Height = DefaultHeight) + { + public string FileName => Path.GetFileName(FullPath.CleanUpPath()); + public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); + } + + private const int DefaultWidth = 450; + private const int DefaultHeight = 80; + + public GUIImage ObjectiveStateIndicator; + public GUIButton ObjectiveButton; + public GUITextBlock LinkedTextBlock; + public LocalizedString ObjectiveText; + + public readonly Identifier Id; + public readonly Text TextContent; + public readonly Video VideoContent; + public readonly AutoPlayVideo AutoPlayVideo; + + public Action OnClickObjective; + + public bool IsCompleted { get; set; } + + public bool CanBeCompleted { get; set; } + + public Identifier ParentId { get; set; } + + public TutorialSegmentType SegmentType { get; private set; } + + public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); + } + + public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + return new Segment(id, objectiveTextTag, onClickObjective); + } + + public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) + { + return new Segment(id, objectiveTextTag); + } + + private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + AutoPlayVideo = autoPlayVideo; + TextContent = textContent; + VideoContent = videoContent; + SegmentType = TutorialSegmentType.InfoBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + OnClickObjective = onClickObjective; + SegmentType = TutorialSegmentType.MessageBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + SegmentType = TutorialSegmentType.Objective; + } + + public void ConnectMessageBox(Segment messageBoxSegment) + { + SegmentType = TutorialSegmentType.MessageBox; + OnClickObjective = messageBoxSegment.OnClickObjective; + } + } + + private readonly record struct ScreenSettings( + Point ScreenResolution = default, + float UiScale = default, + WindowMode WindowMode = default) + { + public bool HaveChanged() => + GameMain.GraphicsWidth != ScreenResolution.X || + GameMain.GraphicsHeight != ScreenResolution.Y || + GUI.Scale != UiScale || + GameSettings.CurrentConfig.Graphics.DisplayMode != WindowMode; + }; + + private const float ObjectiveComponentAnimationTime = 1.5f; + + public static bool ContentRunning { get; private set; } + + public static VideoPlayer VideoPlayer { get; } = new VideoPlayer(); + + private static Segment ActiveContentSegment { get; set; } + + private readonly static List activeObjectives = new List(); + private static GUIComponent infoBox; + private static Action infoBoxClosedCallback; + private static ScreenSettings screenSettings; + private static GUILayoutGroup objectiveGroup; + private static LocalizedString objectiveTextTranslated; + + public static void AddToGUIUpdateList() + { + if (screenSettings.HaveChanged()) + { + CreateObjectiveFrame(); + } + if (activeObjectives.Count > 0 && GameMain.GameSession?.Campaign is not { ShowCampaignUI: true }) + { + objectiveGroup?.AddToGUIUpdateList(order: -1); + } + infoBox?.AddToGUIUpdateList(order: 100); + VideoPlayer.AddToGUIUpdateList(order: 100); + } + + public static void TriggerTutorialSegment(Segment segment, bool connectObjective = false) + { + if (segment.SegmentType != TutorialSegmentType.InfoBox) + { + activeObjectives.Add(segment); + AddToObjectiveList(segment, connectObjective); + return; + } + + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + + var title = TextManager.Get(segment.Id); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); + tutorialText = TextManager.ParseInputTypes(tutorialText); + + switch (segment.AutoPlayVideo) + { + case AutoPlayVideo.Yes: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: LoadActiveContentVideo); + break; + case AutoPlayVideo.No: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: StopCurrentContentSegment, + onVideoButtonClicked: LoadActiveContentVideo); + break; + } + } + + public static void CompleteTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted) + { + return; + } + if (!MarkSegmentCompleted(segment)) + { + return; + } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segmentId}:Completed"); + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:CampaignMode:{segmentId}:Completed"); + campaign?.CampaignMetadata?.SetValue(segmentId, true); + } + } + + public static bool MarkSegmentCompleted(Segment segment, bool flash = true) + { + segment.IsCompleted = true; + if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) + { + if (segment.ObjectiveStateIndicator.Style == style) + { + return false; + } + segment.ObjectiveStateIndicator.ApplyStyle(style); + } + if (flash) + { + segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); + } + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + return true; + } + + public static void RemoveTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment) + { + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{tutorialMode.Tutorial?.Identifier}\" but it isn't active!"); + } + return; + } + segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); + segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); + var parent = segment.LinkedTextBlock.Parent; + parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => + { + activeObjectives.Remove(segment); + objectiveGroup?.Recalculate(); + }); + parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + } + + public static void CloseActiveContentGUI() + { + if (VideoPlayer.IsPlaying) + { + VideoPlayer.Stop(); + } + else if (infoBox != null) + { + CloseInfoFrame(); + } + } + + public static void ClearContent() + { + ContentRunning = false; + infoBox = null; + } + + public static void ResetUI() + { + ContentRunning = false; + infoBox = null; + VideoPlayer.Remove(); + } + + #region Objectives + private static Segment GetActiveObjective(Identifier id) => activeObjectives.FirstOrDefault(s => s.Id == id); + + public static void ResetObjectives() + { + activeObjectives.Clear(); + ActiveContentSegment = null; + CreateObjectiveFrame(); + } + + /// + /// Create the objective list that holds the objectives (called on start and on resolution change) + /// + private static void CreateObjectiveFrame() + { + var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null) + { + CanBeFocused = false + }; + objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) + { + AbsoluteSpacing = (int)GUIStyle.Font.LineHeight + }; + for (int i = 0; i < activeObjectives.Count; i++) + { + AddToObjectiveList(activeObjectives[i], useExistingIndex: true); + } + screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode); + } + + /// + /// Stops content running and adds the active segment to the objective list + /// + private static void StopCurrentContentSegment() + { + if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) + { + activeObjectives.Add(ActiveContentSegment); + AddToObjectiveList(ActiveContentSegment); + } + ContentRunning = false; + ActiveContentSegment = null; + } + + /// + /// Adds the segment to the objective list + /// + private static void AddToObjectiveList(Segment segment, bool connectExisting = false, bool useExistingIndex = false) + { + if (connectExisting) + { + if (activeObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) + { + existingSegment.ConnectMessageBox(segment); + SetButtonBehavior(existingSegment); + } + return; + } + + var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) + { + MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) + }; + Segment parentSegment = activeObjectives.FirstOrDefault(s => s.Id == segment.ParentId); + if (parentSegment is not null) + { + // Add this child as the last child in case there are other existing children already + int childIndex = useExistingIndex ? activeObjectives.IndexOf(segment) : + activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId); + if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) + { + frameRt.RepositionChildInHierarchy(childIndex); + activeObjectives.Remove(segment); + activeObjectives.Insert(childIndex, segment); + } + } + frameRt.AbsoluteOffset = GetObjectiveHiddenPosition(); + + var frame = new GUIFrame(frameRt, style: null) + { + CanBeFocused = true + }; + + objectiveGroup.Recalculate(); + + int textWidth = parentSegment is null ? frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing + : frameRt.Rect.Width - 2 * objectiveGroup.AbsoluteSpacing; + segment.LinkedTextBlock = new GUITextBlock( + new RectTransform(new Point(textWidth, 0), frame.RectTransform, anchor: Anchor.TopRight), + TextManager.ParseInputTypes(segment.ObjectiveText), + wrap: true); + + var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); + segment.LinkedTextBlock.RectTransform.NonScaledSize = size; + segment.LinkedTextBlock.RectTransform.MinSize = size; + segment.LinkedTextBlock.RectTransform.MaxSize = size; + segment.LinkedTextBlock.RectTransform.IsFixedSize = true; + frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); + frame.RectTransform.IsFixedSize = true; + + var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); + if (parentSegment is not null) + { + indicatorRt.AbsoluteOffset = new Point(objectiveGroup.AbsoluteSpacing, 0); + } + segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); + + SetTransparent(segment.LinkedTextBlock); + + objectiveTextTranslated ??= TextManager.Get("Tutorial.Objective"); + segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) + { + ToolTip = objectiveTextTranslated + }; + SetButtonBehavior(segment); + SetTransparent(segment.ObjectiveButton); + + frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); + + // Check if the objective has already been completed in the campaign + if (!segment.IsCompleted && GameMain.GameSession?.Campaign?.CampaignMetadata is CampaignMetadata data && data.GetBoolean(segment.Id)) + { + MarkSegmentCompleted(segment, flash: false); + } + + static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; + + void SetButtonBehavior(Segment segment) + { + segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; + segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => + { + if (segment.SegmentType == TutorialSegmentType.InfoBox) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + ReplaySegmentVideo(segment); + } + else + { + ShowSegmentText(segment); + } + } + else if (segment.SegmentType == TutorialSegmentType.MessageBox) + { + segment.OnClickObjective?.Invoke(); + } + return true; + }; + } + } + + private static void ReplaySegmentVideo(Segment segment) + { + if (ContentRunning) { return; } + Inventory.DraggingItems.Clear(); + ContentRunning = true; + LoadVideo(segment); + } + + private static void ShowSegmentText(Segment segment) + { + if (ContentRunning) { return; } + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + infoBox = CreateInfoFrame( + TextManager.Get(segment.Id), + TextManager.Get(segment.TextContent.Tag), + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: () => ContentRunning = false, + onVideoButtonClicked: () => LoadVideo(segment)); + } + + private static Point GetObjectiveHiddenPosition(RectTransform rt = null) + { + return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); + } + + public static Segment GetObjective(Identifier identifier) + { + return activeObjectives.FirstOrDefault(o => o.Id == identifier); + } + + public static bool AllActiveObjectivesCompleted() + { + return activeObjectives.None() || activeObjectives.All(o => !o.CanBeCompleted || o.IsCompleted); + } + + public static bool AnyObjectives => activeObjectives.Any(); + + #endregion + + #region InfoFrame + + private static void CloseInfoFrame() => CloseInfoFrame(null, null); + + private static bool CloseInfoFrame(GUIButton button, object userData) + { + infoBox = null; + infoBoxClosedCallback?.Invoke(); + return true; + } + + /// + // Creates and displays a tutorial info box + /// + private static GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) + { + if (hasButton) + { + height += 60; + } + + width = (int)(width * GUI.Scale); + height = (int)(height * GUI.Scale); + + LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); + height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; + + if (title.Length > 0) + { + height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); + } + + var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); + infoBlock.Flash(GUIStyle.Green); + + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + + if (title.Length > 0) + { + var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), + title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); + titleBlock.RectTransform.IsFixedSize = true; + } + + text = RichString.Rich(text); + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); + + textBlock.RectTransform.IsFixedSize = true; + infoBoxClosedCallback = onInfoBoxClosed; + + if (hasButton) + { + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.1f + }; + buttonContainer.RectTransform.IsFixedSize = true; + + if (onVideoButtonClicked != null) + { + buttonContainer.Stretch = true; + var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("Video"), style: "GUIButtonLarge") + { + OnClicked = (GUIButton button, object obj) => + { + onVideoButtonClicked(); + return true; + } + }; + } + else + { + buttonContainer.Stretch = false; + buttonContainer.ChildAnchor = Anchor.Center; + } + + var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("OK"), style: "GUIButtonLarge") + { + OnClicked = CloseInfoFrame + }; + } + + infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); + + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + + return background; + } + + #endregion + + #region Video + + private static void LoadVideo(Segment segment) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + VideoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), + contentId: segment.Id, + startPlayback: true, + objective: segment.ObjectiveText, + onStop: StopCurrentContentSegment); + } + else + { + VideoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: null, + contentId: segment.Id, + startPlayback: true, + objective: string.Empty); + } + } + + private static void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); + + #endregion +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index eed8abac4..9da10c685 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -117,7 +118,7 @@ namespace Barotrauma private void UpdateBar() { double elapsedTime = (DateTime.Now - startTime).TotalSeconds; - if (msgBox != null && !msgBox.Closed && GUIMessageBox.MessageBoxes.Contains(msgBox)) + if (msgBox is { Closed: false } && GUIMessageBox.MessageBoxes.Contains(msgBox)) { if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar) { @@ -129,7 +130,7 @@ namespace Barotrauma int second = (int)Math.Ceiling(elapsedTime); if (second > lastSecond) { - if (msgBox != null && !msgBox.Closed) + if (msgBox is { Closed: false }) { SoundPlayer.PlayUISound(GUISoundType.PopupMenu); } @@ -137,6 +138,19 @@ namespace Barotrauma } } + private static void CloseLingeringPopups() + { + foreach (GUIComponent box in GUIMessageBox.MessageBoxes.ToImmutableArray()) + { + if (box is not GUIMessageBox msgBox) { continue; } + + if (msgBox.UserData is PromptData or ResultData) + { + msgBox.Close(); + } + } + } + public static void ClientRead(IReadMessage inc) { ReadyCheckState state = (ReadyCheckState)inc.ReadByte(); @@ -154,6 +168,8 @@ namespace Barotrauma switch (state) { case ReadyCheckState.Start: + CloseLingeringPopups(); + bool isOwn = false; byte authorId = 0; @@ -175,8 +191,8 @@ namespace Barotrauma clients.Add(inc.ReadByte()); } - ReadyCheck rCheck = new ReadyCheck(clients, - DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, + ReadyCheck rCheck = new ReadyCheck(clients, + DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime); crewManager.ActiveReadyCheck = rCheck; @@ -224,7 +240,7 @@ namespace Barotrauma if (IsFinished) { return; } IsFinished = true; - int readyCount = Clients.Count(pair => pair.Value == ReadyStatus.Yes); + int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } @@ -238,31 +254,29 @@ namespace Barotrauma if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; } - if (resultsBox.Content.FindChild(UserListData) is GUIListBox userList) + if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; } + + // for some reason FindChild doesn't work here? + foreach (GUIComponent child in userList.Content.Children) { - // for some reason FindChild doesn't work here? - foreach (GUIComponent child in userList.Content.Children) + if (child.UserData is not byte b || b != id) { continue; } + + if (child.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { continue; } + + string style; + switch (status) { - if (!(child.UserData is byte b) || b != id) { continue; } - - if (child.GetChild().FindChild(ReadySpriteData) is GUIImage image) - { - string style; - switch (status) - { - case ReadyStatus.Yes: - style = "MissionCompletedIcon"; - break; - case ReadyStatus.No: - style = "MissionFailedIcon"; - break; - default: - return; - } - - image.ApplyStyle(GUIStyle.GetComponentStyle(style)); - } + case ReadyStatus.Yes: + style = "MissionCompletedIcon"; + break; + case ReadyStatus.No: + style = "MissionFailedIcon"; + break; + default: + return; } + + image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index a4ec49919..da2c6b35b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -506,7 +506,7 @@ namespace Barotrauma private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { - string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name; + string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; string textTag; if (gameOver) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 1645e1be1..05a85c692 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -774,7 +774,6 @@ namespace Barotrauma } else { - bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); var selectedContainer = character.SelectedItem?.GetComponent(); if (selectedContainer != null && @@ -802,8 +801,7 @@ namespace Barotrauma } else if (character.HeldItems.Any(i => i.OwnInventory != null && - /*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/ - ((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -973,11 +971,11 @@ namespace Barotrauma //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //(in that case, the quick action should just fill up the stack) bool disallowSwapping = - heldItem.OwnInventory.Capacity == 1 && + (heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) && heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || - (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) + ((heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) @@ -1133,7 +1131,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true); var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite(); lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f)); - if (inventoryArea.Contains(PlayerInput.MousePosition)) + if (inventoryArea.Contains(PlayerInput.MousePosition) && character.LockHands) { GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height))); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index 1684642d0..fd10eb0f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -25,14 +25,40 @@ namespace Barotrauma.Items.Components foreach (Node node in nodes) { GameMain.ParticleManager.CreateParticle("swirlysmoke", node.WorldPosition, Vector2.Zero); + + if (node.ParentIndex > -1) + { + CreateParticlesBetween(nodes[node.ParentIndex].WorldPosition, node.WorldPosition); + } + } + foreach (var character in charactersInRange) + { + CreateParticlesBetween(character.character.WorldPosition, character.node.WorldPosition); + } + + static void CreateParticlesBetween(Vector2 start, Vector2 end) + { + const float ParticleInterval = 50.0f; + Vector2 diff = end - start; + float dist = diff.Length(); + Vector2 normalizedDiff = MathUtils.NearlyEqual(dist, 0.0f) ? Vector2.Zero : diff / dist; + for (float x = 0.0f; x < dist; x += ParticleInterval) + { + var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", start + normalizedDiff * x, Vector2.Zero); + if (spark != null) + { + spark.Size *= 0.3f; + } + } } } public void DrawElectricity(SpriteBatch spriteBatch) { + if (timer <= 0.0f) { return; } for (int i = 0; i < nodes.Count; i++) { - if (nodes[i].Length <= 1.0f) continue; + if (nodes[i].Length <= 1.0f) { continue; } var node = nodes[i]; electricitySprite.Draw(spriteBatch, (i + frameOffset) % electricitySprite.FrameCount, @@ -46,10 +72,16 @@ namespace Barotrauma.Items.Components if (GameMain.DebugDraw) { - for (int i = 0; i < nodes.Count; i++) + for (int i = 1; i < nodes.Count; i++) { - if (nodes[i].Length <= 1.0f) continue; - GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 5, Color.LightCyan, isFilled: true); + GUI.DrawLine(spriteBatch, + new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), + new Vector2(nodes[nodes[i].ParentIndex].WorldPosition.X, -nodes[nodes[i].ParentIndex].WorldPosition.Y), + Color.LightCyan, + width: 3); + + if (nodes[i].Length <= 1.0f) { continue; } + GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 10, Color.LightCyan, isFilled: true); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 8a605499d..fd8f360eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -114,8 +114,8 @@ namespace Barotrauma.Items.Components { if (chargeSound != null) { - chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); - if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier()); + if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; } } } else if (chargeSoundChannel != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index f97cdc3be..93769ab4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -224,68 +224,59 @@ namespace Barotrauma.Items.Components if (character == null) { return false; } if (character == Character.Controlled) { - if (targetSections.Count == 0) { return false; } - Spray(deltaTime); + Spray(character, deltaTime, applyColors: targetSections.Count > 0); return true; } else { //allow remote players to use the sprayer, but don't actually color the walls (we'll receive the data from the server) - return character.IsRemotePlayer; + Spray(character, deltaTime, applyColors: false); + return true; } } - public void Spray(float deltaTime) + public void Spray(Character user, float deltaTime, bool applyColors) { - if (targetSections.Count == 0) { return; } - Item liquidItem = liquidContainer?.Inventory.FirstOrDefault(); if (liquidItem == null) { return; } bool isCleaning = false; liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); - // Ethanol or other cleaning solvent - if (color.A == 0) { isCleaning = true; } - - float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; - - if (!isCleaning) + if (applyColors && targetSections.Any()) { - for (int i = 0; i < targetSections.Count; i++) + // Ethanol or other cleaning solvent + if (color.A == 0) { isCleaning = true; } + float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; + if (!isCleaning) { - targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentCleaning += deltaTime; + } } - if (GameMain.GameSession != null) + else { - GameMain.GameSession.TimeSpentCleaning += deltaTime; - } - } - else - { - for (int i = 0; i < targetSections.Count; i++) - { - targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); - } - if (GameMain.GameSession != null) - { - GameMain.GameSession.TimeSpentPainting += deltaTime; + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); + } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentPainting += deltaTime; + } } } Vector2 particleStartPos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - Vector2 particleEndPos = Vector2.Zero; - for (int i = 0; i < targetSections.Count; i++) - { - particleEndPos += new Vector2(targetSections[i].Rect.Center.X, targetSections[i].Rect.Y - targetSections[i].Rect.Height / 2) + targetHull.Rect.Location.ToVector2(); - } - particleEndPos /= targetSections.Count; - if (targetHull?.Submarine != null) - { - particleEndPos += targetHull.Submarine.Position; - } - float dist = Vector2.Distance(particleStartPos, particleEndPos); - + Vector2 particleEndPos = user.CursorWorldPosition; + //the cursor position is not exact for remote players, we only know the direction they're aiming at but not the distance + // -> use 50% range, looks good enough + float dist = Math.Min(Vector2.Distance(particleStartPos, particleEndPos), Range * 0.5f); foreach (ParticleEmitter particleEmitter in particleEmitters) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index d43d947fd..d8c379c03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -309,21 +309,53 @@ namespace Barotrauma.Items.Components Vector2 currentItemPos = transformedItemPos; - SpriteEffects spriteEffects = SpriteEffects.None; - if ((item.body != null && item.body.Dir == -1) || item.FlippedX) - { - spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; - } - if (item.FlippedY) - { - spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; - } - bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; foreach (Item containedItem in Inventory.AllItems) { + Vector2 itemPos = currentItemPos; + var relatedItem = FindContainableItem(containedItem); + if (relatedItem != null) + { + if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } + if (relatedItem.ItemPos.HasValue) + { + Vector2 pos = relatedItem.ItemPos.Value; + if (item.body != null) + { + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) + { + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; + } + if (item.FlippedY) + { + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (item.Submarine != null) + { + itemPos += item.Submarine.DrawPosition; + } + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; + } + } + } + } + if (containedItem?.Sprite == null) { continue; } if (AutoInteractWithContained) @@ -343,19 +375,34 @@ namespace Barotrauma.Items.Components } containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; + SpriteEffects spriteEffects = SpriteEffects.None; + float spriteRotation = ItemRotation; + if (relatedItem != null && relatedItem.Rotation != 0) + { + spriteRotation = relatedItem.Rotation; + } + if ((item.body != null && item.body.Dir == -1) || item.FlippedX) + { + spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; + } + if (item.FlippedY) + { + spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; + } + containedItem.Sprite.Draw( spriteBatch, - new Vector2(currentItemPos.X, -currentItemPos.Y), + new Vector2(itemPos.X, -itemPos.Y), isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation ), + -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), containedItem.Scale, spriteEffects, depth: containedSpriteDepth); foreach (ItemContainer ic in containedItem.GetComponents()) { - if (ic.hideItems) continue; + if (ic.hideItems) { continue; } ic.DrawContainedItems(spriteBatch, containedSpriteDepth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 332f924cd..1d4db25d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { - if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX && item.body == null) + if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX) { Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 16e766617..3debe66f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -14,7 +14,10 @@ namespace Barotrauma.Items.Components private GUIFrame selectedItemFrame; private GUIFrame selectedItemReqsFrame; - + + private GUITextBlock amountTextMin, amountTextMax; + private GUIScrollBar amountInput; + public GUIButton ActivateButton { get { return activateButton; } @@ -160,14 +163,46 @@ namespace Barotrauma.Items.Components new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; // === ACTIVATE BUTTON === // - var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); - activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), - TextManager.Get(CreateButtonText), style: "DeviceButtonFixedSize") + var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.9f), inputArea.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + var amountInputHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), buttonFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + amountTextMin = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center); + + amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider") + { + OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + scrollBar.Step = 1.0f / Math.Max(scrollBar.Range.Y - 1, 1); + AmountToFabricate = (int)MathF.Round(scrollBar.BarScrollValue); + RefreshActivateButtonText(); + if (GameMain.Client != null) + { + item.CreateClientEvent(this); + } + return true; + } + }; + + amountTextMax = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center); + + activateButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.6f), buttonFrame.RectTransform), + TextManager.Get(CreateButtonText), style: "DeviceButton") { OnClicked = StartButtonClicked, UserData = selectedItem, Enabled = false - }; + }; + + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), buttonFrame.RectTransform), style: null); } else { @@ -192,6 +227,21 @@ namespace Barotrauma.Items.Components CreateRecipes(); } + private void RefreshActivateButtonText() + { + if (amountInput == null) + { + activateButton.Text = TextManager.Get(IsActive ? "FabricatorCancel" : CreateButtonText); + } + else + { + activateButton.Text = + IsActive ? + $"{TextManager.Get("FabricatorCancel")} ({amountRemaining})" : + $"{TextManager.Get(CreateButtonText)} ({AmountToFabricate})"; + } + } + partial void CreateRecipes() { itemList.Content.RectTransform.ClearChildren(); @@ -247,7 +297,7 @@ namespace Barotrauma.Items.Components outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; } - private LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) + private static LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) { if (fabricationRecipe == null) { return ""; } if (fabricationRecipe.Amount > 1) @@ -269,7 +319,7 @@ namespace Barotrauma.Items.Components partial void SelectProjSpecific(Character character) { - var nonItems = itemList.Content.Children.Where(c => !(c.UserData is FabricationRecipe)).ToList(); + var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList(); nonItems.ForEach(i => itemList.Content.RemoveChild(i)); itemList.Content.RectTransform.SortChildren((c1, c2) => @@ -277,17 +327,20 @@ namespace Barotrauma.Items.Components var item1 = c1.GUIComponent.UserData as FabricationRecipe; var item2 = c2.GUIComponent.UserData as FabricationRecipe; - int itemPlacement1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f ? 0 : -1; - int itemPlacement2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f ? 0 : -1; - - itemPlacement1 += item1.RequiresRecipe && !character.HasRecipeForItem(item1.TargetItem.Identifier) ? -2 : 0; - itemPlacement2 += item2.RequiresRecipe && !character.HasRecipeForItem(item2.TargetItem.Identifier) ? -2 : 0; - + int itemPlacement1 = calculatePlacement(item1); + int itemPlacement2 = calculatePlacement(item2); if (itemPlacement1 != itemPlacement2) { return itemPlacement1 > itemPlacement2 ? -1 : 1; } + int calculatePlacement(FabricationRecipe recipe) + { + int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1; + placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0; + return placement; + } + return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value); }); @@ -322,7 +375,9 @@ namespace Barotrauma.Items.Components AutoScaleHorizontal = true, CanBeFocused = false }; - var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && (fabricableItem.RequiresRecipe && !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))); + var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => + c.UserData is FabricationRecipe fabricableItem && + fabricableItem.RequiresRecipe && !AnyOneHasRecipeForItem(character, fabricableItem.TargetItem)); if (firstRequiresRecipe != null) { requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform)); @@ -567,7 +622,7 @@ namespace Barotrauma.Items.Components bool recipeVisible = false; foreach (GUIComponent child in itemList.Content.Children.Reverse()) { - if (!(child.UserData is FabricationRecipe recipe)) + if (child.UserData is not FabricationRecipe recipe) { if (child.Enabled) { @@ -598,9 +653,23 @@ namespace Barotrauma.Items.Components { this.selectedItem = selectedItem; + int max = Math.Max(selectedItem.TargetItem.MaxStackSize / selectedItem.Amount, 1); + + if (amountInput != null) + { + float prevBarScroll = amountInput.BarScroll; + amountInput.Range = new Vector2(1, max); + amountInput.BarScroll = prevBarScroll; + + amountTextMax.Text = max.ToString(); + amountInput.Enabled = amountTextMax.Enabled = max > 1; + AmountToFabricate = Math.Min((int)amountInput.BarScrollValue, max); + } + RefreshActivateButtonText(); + selectedItemFrame.ClearChildren(); selectedItemReqsFrame.ClearChildren(); - + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; @@ -734,7 +803,9 @@ namespace Barotrauma.Items.Components outputSlot.Flash(GUIStyle.Red); return false; } - + + amountRemaining = AmountToFabricate; + if (GameMain.Client != null) { pendingFabricatedItem = fabricatedItem != null ? null : selectedItem; @@ -777,7 +848,7 @@ namespace Barotrauma.Items.Components { foreach (GUIComponent child in itemList.Content.Children) { - if (!(child.UserData is FabricationRecipe recipe)) { continue; } + if (child.UserData is not FabricationRecipe recipe) { continue; } if (recipe != selectedItem && (child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y)) @@ -811,11 +882,14 @@ namespace Barotrauma.Items.Components { uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; msg.WriteUInt32(recipeHash); + msg.WriteRangedInteger(AmountToFabricate, 1, MaxAmountToFabricate); } public void ClientEventRead(IReadMessage msg, float sendingTime) { FabricatorState newState = (FabricatorState)msg.ReadByte(); + int amountToFabricate = msg.ReadRangedInteger(0, MaxAmountToFabricate); + int amountRemaining = msg.ReadRangedInteger(0, MaxAmountToFabricate); float newTimeUntilReady = msg.ReadSingle(); uint recipeHash = msg.ReadUInt32(); UInt16 userID = msg.ReadUInt16(); @@ -828,6 +902,8 @@ namespace Barotrauma.Items.Components } State = newState; + this.amountToFabricate = amountToFabricate; + this.amountRemaining = amountRemaining; if (newState == FabricatorState.Stopped || recipeHash == 0) { CancelFabricating(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 3b25c87b0..973f6e514 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -144,6 +144,9 @@ namespace Barotrauma.Items.Components partial class MiniMap : Powered { + private Dictionary hullDatas; + private DateTime resetDataTime; + private GUIFrame submarineContainer; private GUIFrame? hullInfoFrame; @@ -226,6 +229,8 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific() { + hullDatas = new Dictionary(); + SetDefaultMode(); noPowerTip = TextManager.Get("SteeringNoPowerTip"); @@ -408,6 +413,8 @@ namespace Barotrauma.Items.Components var wire = it.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } + if (it.Container?.GetComponent() is { DrawInventory: false }) { return false; } + if (it.HasTag("traitormissionitem")) { return false; } return true; @@ -549,6 +556,34 @@ namespace Barotrauma.Items.Components CreateHUD(); } + //reset data if we haven't received anything in a while + //(so that outdated hull info won't be shown if detectors stop sending signals) + if (DateTime.Now > resetDataTime) + { + foreach (HullData hullData in hullDatas.Values) + { + if (!hullData.Distort) + { + if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; } + if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; } + } + } + resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); + } + + if (cardRefreshTimer > cardRefreshDelay) + { + if (item.Submarine is { } sub) + { + UpdateIDCards(sub); + } + cardRefreshTimer = 0; + } + else + { + cardRefreshTimer += deltaTime; + } + if (scissorComponent != null) { if (PlayerInput.PrimaryMouseButtonDown() && currentMode != MiniMapMode.HullStatus) @@ -952,11 +987,8 @@ namespace Barotrauma.Items.Components component.Color = borderComponent.OutlineColor = NoPowerColor; } - if (Voltage < MinVoltage) { continue; } - if (!component.Visible) { continue; } - if (!(entity is Hull hull)) { continue; } - + if (entity is not Hull hull) { continue; } if (!submarineContainer.Rect.Contains(component.Rect)) { if (hull.Submarine.Info.Type != SubmarineType.Player) @@ -966,6 +998,8 @@ namespace Barotrauma.Items.Components } } + if (Voltage < MinVoltage) { continue; } + hullDatas.TryGetValue(hull, out HullData? hullData); if (hullData is null) { @@ -1119,7 +1153,7 @@ namespace Barotrauma.Items.Components if (it.GetComponent() is { } battery) { - int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f); + int batteryCapacity = (int)(battery.Charge / battery.GetCapacity() * 100f); line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString()); } else if (it.GetComponent() is { } powerTransfer) @@ -1734,6 +1768,67 @@ namespace Barotrauma.Items.Components return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); } + public override void ReceiveSignal(Signal signal, Connection connection) + { + Item source = signal.source; + if (source == null || source.CurrentHull == null) { return; } + + Hull sourceHull = source.CurrentHull; + if (!hullDatas.TryGetValue(sourceHull, out HullData? hullData)) + { + hullData = new HullData(); + hullDatas.Add(sourceHull, hullData); + } + + if (hullData.Distort) { return; } + + switch (connection.Name) + { + case "water_data_in": + //cheating a bit because water detectors don't actually send the water level + bool fromWaterDetector = source.GetComponent() != null; + hullData.ReceivedWaterAmount = null; + hullData.LastWaterDataTime = Timing.TotalTime; + if (fromWaterDetector) + { + hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); + } + foreach (var linked in sourceHull.linkedTo) + { + if (linked is not Hull linkedHull) { continue; } + if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData)) + { + linkedHullData = new HullData(); + hullDatas.Add(linkedHull, linkedHullData); + } + linkedHullData.ReceivedWaterAmount = null; + if (fromWaterDetector) + { + linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull); + } + } + break; + case "oxygen_data_in": + if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy)) + { + oxy = Rand.Range(0.0f, 100.0f); + } + hullData.ReceivedOxygenAmount = oxy; + hullData.LastOxygenDataTime = Timing.TotalTime; + foreach (var linked in sourceHull.linkedTo) + { + if (linked is not Hull linkedHull) { continue; } + if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData)) + { + linkedHullData = new HullData(); + hullDatas.Add(linkedHull, linkedHullData); + } + linkedHullData.ReceivedOxygenAmount = oxy; + } + break; + } + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index b229b2142..9705b407b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -29,7 +29,11 @@ namespace Barotrauma.Items.Components private Sprite tempRangeIndicator; private Sprite graphLine; - //private GUIFrame graph; + private GUICustomComponent graph; + + private GUIFrame inventoryWindow; + private GUILayoutGroup buttonArea; + private GUIFrame infographic; private Color optimalRangeColor = new Color(74,238,104,255); private Color offRangeColor = Color.Orange; @@ -66,6 +70,8 @@ namespace Barotrauma.Items.Components }; public override bool RecreateGUIOnResolutionChange => true; + + public bool TriggerInfographic { get; set; } partial void InitProjSpecific(ContentXElement element) { @@ -122,7 +128,7 @@ namespace Barotrauma.Items.Components //left column //---------------------------------------------------------- - GUIFrame inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight) + inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight) { MinSize = new Point(85, 220), RelativeOffset = new Vector2(-0.02f, 0) @@ -255,7 +261,7 @@ namespace Barotrauma.Items.Components }; TurbineOutputScrollBar.Frame.UserData = UIHighlightAction.ElementId.TurbineOutputSlider; - var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform)) + buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform)) { Stretch = true, RelativeSpacing = 0.02f @@ -390,7 +396,9 @@ namespace Barotrauma.Items.Components ToolTip = TextManager.Get("reactor.temperatureboostup"), OnClicked = (_, __) => { - applyTemperatureBoost(TemperatureBoostAmount, temperatureBoostSoundUp); + unsentChanges = true; + sendUpdateTimer = 0.0f; + ApplyTemperatureBoost(TemperatureBoostAmount); return true; } }; @@ -401,25 +409,13 @@ namespace Barotrauma.Items.Components ToolTip = TextManager.Get("reactor.temperatureboostdown"), OnClicked = (_, __) => { - applyTemperatureBoost(-TemperatureBoostAmount, temperatureBoostSoundDown); + unsentChanges = true; + sendUpdateTimer = 0.0f; + ApplyTemperatureBoost(-TemperatureBoostAmount); return true; } }; - void applyTemperatureBoost(float amount, RoundSound sound) - { - temperatureBoost = amount; - if (sound != null) - { - SoundPlayer.PlaySound( - sound.Sound, - item.WorldPosition, - sound.Volume, - sound.Range, - hullGuess: item.CurrentHull); - } - } - var graphArea = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), bottomRightArea.RectTransform)) { Stretch = true, @@ -436,8 +432,8 @@ namespace Barotrauma.Items.Components LocalizedString kW = TextManager.Get("kilowatt"); loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}"; - var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); - new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null); + var graphFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); + graph = new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graphFrame.RectTransform, Anchor.Center), DrawGraph, null); var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform), "Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) @@ -448,6 +444,41 @@ namespace Barotrauma.Items.Components outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}"; InitInventoryUI(); + + // Infographic overlay --------------------- + int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); + var helpButtonRt = new RectTransform(new Point(buttonHeight), parent: GuiFrame.RectTransform, anchor: Anchor.TopRight) + { + AbsoluteOffset = new Point(buttonHeight / 4), + MinSize = new Point(buttonHeight) + }; + new GUIButton(helpButtonRt, "", style: "HelpIcon") + { + OnClicked = (_, _) => + { + CreateInfrographic(); + return true; + } + }; + } + private void ApplyTemperatureBoost(float amount) + { + if (Math.Abs(temperatureBoost) <= TemperatureBoostAmount * 0.9f && + Math.Abs(amount) > TemperatureBoostAmount * 0.9f) + { + var sound = amount > 0 ? temperatureBoostSoundUp : temperatureBoostSoundDown; + if (sound != null) + { + SoundPlayer.PlaySound( + sound.Sound, + item.WorldPosition, + sound.Volume, + sound.Range, + freqMult: sound.GetRandomFrequencyMultiplier(), + hullGuess: item.CurrentHull); + } + } + temperatureBoost = amount; } private void InitInventoryUI() @@ -469,7 +500,6 @@ namespace Barotrauma.Items.Components InitInventoryUI(); } - private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container) { Vector2 meterPos = new Vector2(container.Rect.X, container.Rect.Y); @@ -518,7 +548,6 @@ namespace Barotrauma.Items.Components DrawGraph(loadGraph, spriteBatch, graphRect, Math.Max(10000.0f, maxLoad), xOffset, loadColor); } - private void UpdateGraph(float deltaTime) { graphTimer += deltaTime * 1000.0f; @@ -645,6 +674,12 @@ namespace Barotrauma.Items.Components } } } + + if (TriggerInfographic) + { + CreateInfrographic(); + TriggerInfographic = false; + } } private void DrawMeter(SpriteBatch spriteBatch, Rectangle rect, Sprite meterSprite, float value, Vector2 range, Vector2 optimalRange, Vector2 allowedRange) @@ -760,7 +795,96 @@ namespace Barotrauma.Items.Components spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred); } + + private enum InfographicArrowStyle { Straight, Curved }; + private void CreateInfrographic() + { + if (infographic != null) { return; } + var dimColor = Color.Lerp(Color.Black, Color.TransparentBlack, 0.25f); + // Dim reactor interface + infographic = new GUIFrame(new RectTransform(Vector2.One, GuiFrame.RectTransform)) + { + CanBeFocused = false, + Color = dimColor + }; + // Dim inventory window + new GUIFrame(new RectTransform(inventoryWindow.Rect.Size, infographic.RectTransform) { AbsoluteOffset = inventoryWindow.Rect.Location - GuiFrame.Rect.Location}, color: dimColor) + { + CanBeFocused = false + }; + int arrowSize = (int)(70 * GUI.Scale); + var arrows = new Dictionary() + { + { "fuelslots", CreateArrow(InfographicArrowStyle.Curved, inventoryWindow, Anchor.TopLeft, Pivot.TopRight, SpriteEffects.FlipVertically) }, + { "temperature", CreateArrow(InfographicArrowStyle.Straight, temperatureBoostDownButton, Anchor.Center, Pivot.Center) }, + { "automaticcontrol", CreateArrow(InfographicArrowStyle.Curved, AutoTempSwitch, Anchor.TopRight, Pivot.BottomRight, rotationDegrees: 90f) }, + { "power", CreateArrow(InfographicArrowStyle.Straight, PowerButton, Anchor.BottomCenter, Pivot.TopCenter) } + }; + CreateArrow(InfographicArrowStyle.Straight, FissionRateScrollBar, Anchor.Center, Pivot.Center); + CreateArrow(InfographicArrowStyle.Straight, TurbineOutputScrollBar, Anchor.Center, Pivot.Center); + CreateArrow(InfographicArrowStyle.Straight, graph, Anchor.TopLeft, Pivot.TopLeft, SpriteEffects.FlipHorizontally, additionalOffset: new Point(arrowSize / 2, 0)); + CreateArrow(InfographicArrowStyle.Straight, graph, Anchor.BottomLeft, Pivot.BottomLeft, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically, additionalOffset: new Point(arrowSize / 2, 0)); + new GUICustomComponent(new RectTransform(Vector2.One, infographic.RectTransform), + onDraw: (sb, c) => + { + DrawToolTip("fuelslots", Anchor.TopLeft, Pivot.BottomCenter, arrows["fuelslots"]); + DrawToolTip("fissionrate", Anchor.TopCenter, Pivot.TopCenter, buttonArea); + DrawToolTip("temperature", Anchor.BottomLeft, Pivot.TopLeft, arrows["temperature"]); + DrawToolTip("automaticcontrol", Anchor.TopLeft, Pivot.CenterRight, arrows["automaticcontrol"]); + DrawToolTip("power", Anchor.BottomCenter, Pivot.TopCenter, arrows["power"]); + DrawToolTip("load", Anchor.CenterLeft, Pivot.CenterLeft, graph); + + void DrawToolTip(string textTag, Anchor anchor, Pivot pivot, GUIComponent targetComponent) + { + GUIComponent.DrawToolTip(sb, + TextManager.Get($"infographic.reactor.{textTag}"), + targetComponent.Rect, + anchor: anchor, + pivot: pivot); + } + }) + { + CanBeFocused = false + }; + var closeButtonRt = new RectTransform(new Point(200, 50).Multiply(GUI.Scale), infographic.RectTransform, Anchor.TopRight, Pivot.BottomRight) + { + AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale) + }; + new GUIButton(closeButtonRt, TextManager.Get("close")) + { + OnClicked = (_, _) => + { + CloseInfographic(Character.Controlled); + return true; + } + }; + item.OnDeselect += CloseInfographic; + + GUIImage CreateArrow(InfographicArrowStyle arrowStyle, GUIComponent parent, Anchor anchor, Pivot pivot, SpriteEffects spriteEffects = SpriteEffects.None, float rotationDegrees = 0f, Point? additionalOffset = null) + { + Point offset = (additionalOffset ?? Point.Zero) + RectTransform.CalculateAnchorPoint(anchor, parent.Rect) - GuiFrame.Rect.Location; + var rt = new RectTransform(new Point(arrowSize), infographic.RectTransform, pivot: pivot) + { + AbsoluteOffset = offset + }; + string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved"; + return new GUIImage(rt, style) + { + Rotation = MathHelper.ToRadians(rotationDegrees), + SpriteEffects = spriteEffects + }; + } + + void CloseInfographic(Character character) + { + if (character != Character.Controlled) { return; } + GuiFrame.RemoveChild(infographic); + infographic = null; + item.OnDeselect -= CloseInfographic; + } + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); @@ -780,6 +904,7 @@ namespace Barotrauma.Items.Components msg.WriteBoolean(PowerOn); msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); + msg.WriteRangedSingle(temperatureBoost, -TemperatureBoostAmount, TemperatureBoostAmount, 8); correctionTimer = CorrectionDelay; } @@ -788,7 +913,7 @@ namespace Barotrauma.Items.Components { if (correctionTimer > 0.0f) { - StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime); + StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8 + 8), sendingTime); return; } @@ -798,6 +923,7 @@ namespace Barotrauma.Items.Components TargetFissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); TargetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); degreeOfSuccess = msg.ReadRangedSingle(0.0f, 1.0f, 8); + ApplyTemperatureBoost(msg.ReadRangedSingle(-TemperatureBoostAmount, TemperatureBoostAmount, 8)); if (Math.Abs(FissionRateScrollBar.BarScroll - TargetFissionRate / 100.0f) > 0.01f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index b6ff975de..628f5a54b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -802,11 +802,12 @@ namespace Barotrauma.Items.Components if (passivePingRadius > 0.0f) { if (activePingsCount == 0) { disruptedDirections.Clear(); } + //emit "pings" from nearby sound-emitting AITargets to reveal what's around them foreach (AITarget t in AITarget.List) { if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } - + float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } @@ -814,9 +815,12 @@ namespace Barotrauma.Items.Components if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { Ping(t.WorldPosition, transducerCenter, - Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), - passive: true, pingStrength: 0.5f); - sonarBlips.Add(new SonarBlip(t.WorldPosition, 1.0f, 1.0f)); + t.SoundRange * displayScale, 0, displayScale, range, + passive: true, pingStrength: 0.5f, needsToBeInSector: t); + if (t.IsWithinSector(transducerCenter)) + { + sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f))); + } } } } @@ -1276,7 +1280,7 @@ namespace Barotrauma.Items.Components float indicatorSector = sector * 0.75f; float indicatorSectorLength = (float)(midLength / Math.Cos(indicatorSector)); - bool withinSector = + bool withinSector = (Math.Abs(diff.X) < steering.ActiveDockingSource.DistanceTolerance.X && Math.Abs(diff.Y) < steering.ActiveDockingSource.DistanceTolerance.Y) || Vector2.Dot(normalizedDockingDir, MathUtils.RotatePoint(normalizedDockingDir, indicatorSector)) < Vector2.Dot(normalizedDockingDir, Vector2.Normalize(dockingDir)); @@ -1345,7 +1349,7 @@ namespace Barotrauma.Items.Components } private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive, - float pingStrength = 1.0f) + float pingStrength = 1.0f, AITarget needsToBeInSector = null) { float prevPingRadiusSqr = prevPingRadius * prevPingRadius; float pingRadiusSqr = pingRadius * pingRadius; @@ -1357,25 +1361,25 @@ namespace Barotrauma.Items.Components new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y), new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y), pingSource, transducerPos, - pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector); CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), pingSource, transducerPos, - pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector); CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y), new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), pingSource, transducerPos, - pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector); CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y), new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), pingSource, transducerPos, - pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector); return; } @@ -1404,7 +1408,8 @@ namespace Barotrauma.Items.Components end + submarine.WorldPosition, pingSource, transducerPos, pingRadius, prevPingRadius, - 200.0f, 2.0f, range, 1.0f, passive); + 200.0f, 2.0f, range, 1.0f, passive, + needsToBeInSector: needsToBeInSector); } } @@ -1417,7 +1422,8 @@ namespace Barotrauma.Items.Components new Vector2(pingSource.X + range, Level.Loaded.Size.Y), pingSource, transducerPos, pingRadius, prevPingRadius, - 250.0f, 150.0f, range, pingStrength, passive); + 250.0f, 150.0f, range, pingStrength, passive, + needsToBeInSector: needsToBeInSector); } if (pingSource.Y - Level.Loaded.BottomPos < range) { @@ -1426,7 +1432,8 @@ namespace Barotrauma.Items.Components new Vector2(pingSource.X + range, Level.Loaded.BottomPos), pingSource, transducerPos, pingRadius, prevPingRadius, - 250.0f, 150.0f, range, pingStrength, passive); + 250.0f, 150.0f, range, pingStrength, passive, + needsToBeInSector: needsToBeInSector); } List cells = Level.Loaded.GetCells(pingSource, 7); @@ -1448,7 +1455,8 @@ namespace Barotrauma.Items.Components pingSource, transducerPos, pingRadius, prevPingRadius, 350.0f, 3.0f * (Math.Abs(facingDot) + 1.0f), range, pingStrength, passive, - blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default); + blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default, + needsToBeInSector: needsToBeInSector); } } } @@ -1458,14 +1466,13 @@ namespace Barotrauma.Items.Components if (item.CurrentHull == null && item.Prefab.SonarSize > 0.0f) { float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared(); - if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) { var blip = new SonarBlip( item.WorldPosition + Rand.Vector(item.Prefab.SonarSize), MathHelper.Clamp(item.Prefab.SonarSize, 0.1f, pingStrength), MathHelper.Clamp(item.Prefab.SonarSize * 0.1f, 0.1f, 10.0f)); - if (!passive && !CheckBlipVisibility(blip, transducerPos)) continue; + if (!IsVisible(blip)) { continue; } sonarBlips.Add(blip); } } @@ -1488,7 +1495,7 @@ namespace Barotrauma.Items.Components c.WorldPosition, MathHelper.Clamp(c.Mass, 0.1f, pingStrength), MathHelper.Clamp(c.Mass * 0.03f, 0.1f, 2.0f)); - if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; } + if (!IsVisible(blip)) { continue; } sonarBlips.Add(blip); HintManager.OnSonarSpottedCharacter(Item, c); } @@ -1505,19 +1512,29 @@ namespace Barotrauma.Items.Components if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) { var blip = new SonarBlip( - limb.WorldPosition + Rand.Vector(limb.Mass / 10.0f), - MathHelper.Clamp(limb.Mass, 0.1f, pingStrength), + limb.WorldPosition + Rand.Vector(limb.Mass / 10.0f), + MathHelper.Clamp(limb.Mass, 0.1f, pingStrength), MathHelper.Clamp(limb.Mass * 0.1f, 0.1f, 2.0f)); - if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; } + if (!IsVisible(blip)) { continue; } sonarBlips.Add(blip); HintManager.OnSonarSpottedCharacter(Item, c); } } } + + bool IsVisible(SonarBlip blip) + { + if (!passive && !CheckBlipVisibility(blip, transducerPos)) { return false; } + if (needsToBeInSector != null) + { + if (!needsToBeInSector.IsWithinSector(blip.Position)) { return false; } + } + return true; + } } - + private void CreateBlipsForLine(Vector2 point1, Vector2 point2, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, - float lineStep, float zStep, float range, float pingStrength, bool passive, BlipType blipType = BlipType.Default) + float lineStep, float zStep, float range, float pingStrength, bool passive, BlipType blipType = BlipType.Default, AITarget needsToBeInSector = null) { lineStep /= zoom; zStep /= zoom; @@ -1563,12 +1580,17 @@ namespace Barotrauma.Items.Components Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / displayScale; float fadeTimer = alpha * (1.0f - displayPointDist / range); - int minDist = (int)(200 / zoom); - sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist); + if (needsToBeInSector != null) + { + if (!needsToBeInSector.IsWithinSector(pos)) { continue; } + } var blip = new SonarBlip(pos, fadeTimer, 1.0f + ((displayPointDist + z) / DisplayRadius), blipType); if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; } + int minDist = (int)(200 / zoom); + sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist); + sonarBlips.Add(blip); zStep += 0.5f / zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 7f06e5dbf..3759e9fd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components private bool? swapDestinationOrder; - private GUIMessageBox enterOutpostPrompt; + private GUIMessageBox enterOutpostPrompt, exitOutpostPrompt; private bool levelStartSelected; public bool LevelStartSelected @@ -382,15 +382,26 @@ namespace Barotrauma.Items.Components DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) { // Undocking from an outpost - campaign.ShowCampaignUI = true; - campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); - return false; + if (!ObjectiveManager.AllActiveObjectivesCompleted()) + { + exitOutpostPrompt = new GUIMessageBox("", + TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.Name), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => + { + exitOutpostPrompt.Close(); + return OpenMap(campaign); + }; + exitOutpostPrompt.Buttons[1].OnClicked += exitOutpostPrompt.Close; + return false; + } + return OpenMap(campaign); } else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && !ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) { // Docking to an outpost - var subsToLeaveBehind = campaign.GetSubsToLeaveBehind(Item.Submarine); + var subsToLeaveBehind = CampaignMode.GetSubsToLeaveBehind(Item.Submarine); if (subsToLeaveBehind.Any()) { enterOutpostPrompt = new GUIMessageBox( @@ -419,6 +430,14 @@ namespace Barotrauma.Items.Components return true; } }; + + bool OpenMap(CampaignMode campaign) + { + campaign.ShowCampaignUI = true; + campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); + return false; + } + void SendDockingSignal() { if (GameMain.Client == null) @@ -431,6 +450,7 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } } + dockingButton.Font = GUIStyle.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; @@ -913,6 +933,7 @@ namespace Barotrauma.Items.Components maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); enterOutpostPrompt?.Close(); + exitOutpostPrompt?.Close(); pathFinder = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 72070c051..13e9d9ffc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)" + TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)adjustedCapacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, adjustedCapacity))} %)" }; if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUIStyle.SmallFont; } @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components { ProgressGetter = () => { - return capacity <= 0.0f ? 1.0f : charge / capacity; + return adjustedCapacity <= 0.0f ? 1.0f : charge / adjustedCapacity; } }; } @@ -126,42 +126,50 @@ namespace Barotrauma.Items.Components { if (chargeIndicator != null) { - float chargeRatio = charge / capacity; + float chargeRatio = charge / adjustedCapacity; chargeIndicator.Color = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); } } public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) { return; } + Vector2 scaledIndicatorSize = indicatorSize * item.Scale; + if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; } + const float outlineThickness = 1.0f; Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale; - Vector2 indicatorPos = -itemSize / 2 + indicatorPosition * item.Scale; - if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; } - if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; } + Vector2 indicatorPos = -itemSize / 2.0f + indicatorPosition * item.Scale; + Vector2 itemPosition = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y); + Vector2 flip = new Vector2(item.FlippedX && item.Prefab.CanSpriteFlipX ? -1.0f : 1.0f, item.FlippedY && item.Prefab.CanSpriteFlipY ? -1.0f : 1.0f); + Matrix rotate = Matrix.CreateRotationZ(item.RotationRad); + Vector2 center = Vector2.Transform((indicatorPos + (scaledIndicatorSize * 0.5f)) * flip, rotate) + itemPosition; - if (charge > 0 && capacity > 0) + if (charge > 0 && adjustedCapacity > 0) { - float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); + float chargeRatio = MathHelper.Clamp(charge / adjustedCapacity, 0.0f, 1.0f); Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); - if (!isHorizontal) + Vector2 indicatorCenter = (indicatorPos + (scaledIndicatorSize * 0.5f)) * flip; + Vector2 indicatorSize; + + if (isHorizontal) { - GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - chargeRatio))) + indicatorPos, - new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * chargeRatio), indicatorColor, true, - depth: item.SpriteDepth - 0.00001f); + float indicatorLength = (scaledIndicatorSize.X - outlineThickness * 2.0f) * chargeRatio; + indicatorCenter.X += -scaledIndicatorSize.X * 0.5f + (flipIndicator ? scaledIndicatorSize.X - outlineThickness - indicatorLength * 0.5f : outlineThickness + indicatorLength * 0.5f); + indicatorSize = new Vector2(indicatorLength, scaledIndicatorSize.Y); } else { - GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, - new Vector2((indicatorSize.X * item.Scale) * chargeRatio, indicatorSize.Y * item.Scale), indicatorColor, true, - depth: item.SpriteDepth - 0.00001f); + float indicatorLength = (scaledIndicatorSize.Y - outlineThickness * 2.0f) * chargeRatio; + indicatorCenter.Y += -scaledIndicatorSize.Y * 0.5f + (flipIndicator ? outlineThickness + indicatorLength * 0.5f : scaledIndicatorSize.Y - outlineThickness - indicatorLength * 0.5f); + indicatorSize = new Vector2(scaledIndicatorSize.X, indicatorLength); } + + indicatorCenter = Vector2.Transform(indicatorCenter, rotate) + itemPosition; + + GUI.DrawFilledRectangle(spriteBatch, indicatorCenter, indicatorSize, indicatorSize * 0.5f, item.RotationRad, indicatorColor, item.SpriteDepth - 0.00001f); } - GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, - indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f); + + GUI.DrawRectangle(spriteBatch, center, scaledIndicatorSize, scaledIndicatorSize * 0.5f, item.RotationRad, Color.Black, item.SpriteDepth - 0.000015f, outlineThickness, GUI.OutlinePosition.Inside); } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData) @@ -185,7 +193,7 @@ namespace Barotrauma.Items.Components rechargeSpeedSlider.BarScroll = rechargeRate; } #endif - Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * capacity; + Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * adjustedCapacity; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 63560b853..915e8f695 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -35,6 +35,13 @@ namespace Barotrauma.Items.Components if (GuiFrame == null) { return; } originalMaxSize = GuiFrame.RectTransform.MaxSize; originalRelativeSize = GuiFrame.RectTransform.RelativeSize; + CreateGUI(); + } + + protected override void CreateGUI() + { + if (GuiFrame == null) { return; } + CheckForLabelOverlap(); var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null) { @@ -43,8 +50,8 @@ namespace Barotrauma.Items.Components content.RectTransform.SetAsFirstChild(); //prevents inputs from going through the GUICustomComponent to the drag handle - dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) - { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); + dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) + { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); } public void TriggerRewiringSound() @@ -121,12 +128,6 @@ namespace Barotrauma.Items.Components } } - protected override void OnResolutionChanged() - { - if (GuiFrame == null) { return; } - CheckForLabelOverlap(); - } - private void CheckForLabelOverlap() { GuiFrame.RectTransform.MaxSize = originalMaxSize; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 17922b0b4..c97a2945e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components if (Character.Controlled != null) { Character.Controlled.FocusedItem = null; - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; Character.Controlled.ClearInputs(); } //cancel dragging @@ -401,7 +401,7 @@ namespace Barotrauma.Items.Components { if (Character.Controlled != null) { - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; Character.Controlled.ClearInputs(); } int closestSectionIndex = selectedWire.GetClosestSectionIndex(mousePos, sectionSelectDist, out _); @@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components { if (Character.Controlled != null) { - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; Character.Controlled.ClearInputs(); } draggingWire = selectedWire; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index d6e5500c3..afe938020 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -293,7 +293,7 @@ namespace Barotrauma.Items.Components if (target.Bleeding > 0.0f) { - int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f) * BleedingTexts.Length, 0, BleedingTexts.Length - 1); + int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f * BleedingTexts.Length), 0, BleedingTexts.Length - 1); texts.Add(BleedingTexts[bleedingTextIndex]); textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, target.Bleeding / 100.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 03e4e2eff..e66a15de4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -212,14 +212,14 @@ namespace Barotrauma.Items.Components { if (moveSoundChannel == null && startMoveSound != null) { - moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling); + moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling, freqMult: startMoveSound.GetRandomFrequencyMultiplier()); } else if (moveSoundChannel == null || !moveSoundChannel.IsPlaying) { if (moveSound != null) { moveSoundChannel.FadeOutAndDispose(); - moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling); + moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling, freqMult: moveSound.GetRandomFrequencyMultiplier()); if (moveSoundChannel != null) moveSoundChannel.Looping = true; } } @@ -231,7 +231,7 @@ namespace Barotrauma.Items.Components if (endMoveSound != null && moveSoundChannel.Sound != endMoveSound.Sound) { moveSoundChannel.FadeOutAndDispose(); - moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling); + moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling, freqMult: endMoveSound.GetRandomFrequencyMultiplier()); if (moveSoundChannel != null) moveSoundChannel.Looping = false; } else if (!moveSoundChannel.IsPlaying) @@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components { if (chargeSound != null) { - chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier()); if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; } } @@ -411,6 +411,14 @@ namespace Barotrauma.Items.Components SpriteEffects.None, newDepth); } + if (GameMain.DebugDraw) + { + Vector2 firingPos = GetRelativeFiringPosition(); + firingPos.Y = -firingPos.Y; + GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitX * 5, firingPos + Vector2.UnitX * 5, Color.Red); + GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitY * 5, firingPos + Vector2.UnitY * 5, Color.Red); + } + if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } const float widgetRadius = 60.0f; @@ -581,7 +589,7 @@ namespace Barotrauma.Items.Components var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } availableCharge += battery.Charge; - availableCapacity += battery.Capacity; + availableCapacity += battery.GetCapacity(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 4d4893e4e..58093440a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; @@ -6,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class Wearable : Pickable, IServerSerializable { - private void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) + private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } @@ -19,8 +20,13 @@ namespace Barotrauma.Items.Components if (!description.IsNullOrWhiteSpace()) { description += '\n'; } description += $" ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; } - + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) + { + AddTooltipInfo(damageModifiers, SkillModifiers, ref description); + } + + public static void AddTooltipInfo(IReadOnlyList damageModifiers, IReadOnlyDictionary skillModifiers, ref LocalizedString description) { if (damageModifiers.Any()) { @@ -41,9 +47,9 @@ namespace Barotrauma.Items.Components } } } - if (SkillModifiers.Any()) + if (skillModifiers.Any()) { - foreach (var skillModifier in SkillModifiers) + foreach (var skillModifier in skillModifiers) { string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green); int roundedValue = (int)Math.Round(skillModifier.Value); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 7858e3154..a11245fe4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1608,22 +1608,79 @@ namespace Barotrauma { containedState = item.Condition / item.MaxCondition; } - else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) - { - containedState = itemContainer.Inventory.AllItems.Count() / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.Capacity); - } else { - var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); - containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? - (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : - itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; - if (containedItem != null && itemContainer.Inventory.Capacity == 1) + int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0); + ItemSlot containedItemSlot = null; + if (targetSlot < itemContainer.Inventory.slots.Length) { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); - if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) + containedItemSlot = itemContainer.Inventory.slots[targetSlot]; + } + if (containedItemSlot != null) + { + Item containedItem = containedItemSlot.FirstOrDefault(); + if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) { - containedState = itemContainer.Inventory.slots[0].Items.Count / (float)maxStackSize; + if (containedItem == null) + { + // No item on the defined slot, check if the items on other slots can be used. + containedItem = containedItemSlot.FirstOrDefault() ?? itemContainer.Inventory.AllItems.FirstOrDefault(it => itemContainer.CanBeContained(it, targetSlot)); + } + if (containedItem != null) + { + int ignoredItemCount = 0; + var subContainableItems = itemContainer.AllSubContainableItems; + float capacity = itemContainer.GetMaxStackSize(targetSlot); + if (subContainableItems != null) + { + bool useMainContainerCapacity = true; + foreach (Item it in itemContainer.Inventory.AllItems) + { + // Ignore all items in the sub containers. + foreach (RelatedItem ri in subContainableItems) + { + if (ri.MatchesItem(containedItem)) + { + // The target item is in a subcontainer -> inverse the logic. + useMainContainerCapacity = false; + break; + } + if (ri.MatchesItem(it)) + { + ignoredItemCount++; + } + } + if (!useMainContainerCapacity) { break; } + } + if (useMainContainerCapacity) + { + capacity *= itemContainer.MainContainerCapacity; + } + else + { + // Ignore all items in the main container. + ignoredItemCount = itemContainer.Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); + capacity *= itemContainer.Capacity - itemContainer.MainContainerCapacity; + } + } + int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItemCount; + containedState = Math.Min(itemCount / Math.Max(capacity, 1), 1); + } + } + else + { + containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? + (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : + itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; + + if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) + { + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot)); + if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) + { + containedState = containedItemSlot.Items.Count / (float)maxStackSize; + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0c35c29eb..9bd922451 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -242,6 +242,11 @@ namespace Barotrauma return false; } + if (parentInventory?.Owner is Character character && character.InvisibleTimer > 0.0f) + { + return false; + } + Rectangle extents; if (cachedVisibleExtents.HasValue) { @@ -1251,11 +1256,9 @@ namespace Barotrauma { foreach (ItemComponent ic in components) { - if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } if (ic is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { continue; } - Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { @@ -1268,6 +1271,7 @@ namespace Barotrauma color = Color.Cyan; } } + if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false)); } } @@ -1282,7 +1286,8 @@ namespace Barotrauma { foreach (ItemComponent ic in activeHUDs) { - if (ic.GuiFrame == null || !ic.CanBeSelected) { continue; } + if (ic.GuiFrame == null) { continue; } + if (!ic.CanBeSelected && !ic.DrawHudWhenEquipped) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; if (ic.UseAlternativeLayout) { @@ -1415,6 +1420,15 @@ namespace Barotrauma case EventType.ChangeProperty: ReadPropertyChange(msg, false); break; + case EventType.ItemStat: + byte length = msg.ReadByte(); + for (int i = 0; i < length; i++) + { + var statIdentifier = INetSerializableStruct.Read(msg); + var statValue = msg.ReadSingle(); + StatManager.ApplyStat(statIdentifier, statValue); + } + break; case EventType.Upgrade: Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 8c6aed81b..647e2dbc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -1,5 +1,5 @@ -using Barotrauma.IO; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -73,6 +72,9 @@ namespace Barotrauma public float UpgradePreviewScale = 1.0f; + private IReadOnlyList wearableDamageModifiers; + private IReadOnlyDictionary wearableSkillModifiers; + //only used to display correct color in the sub editor, item instances have their own property that can be edited on a per-item basis [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] public Color InventoryIconColor { get; protected set; } @@ -101,6 +103,9 @@ namespace Barotrauma var containedSprites = new List(); var decorativeSpriteGroups = new Dictionary>(); + var wearableDamageModifiers = new List(); + var wearableSkillModifiers = new Dictionary(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) @@ -198,8 +203,33 @@ namespace Barotrauma containedSprites.Add(containedSprite); } break; + case "wearable": + foreach (ContentXElement wearableSubElement in subElement.Elements()) + { + switch (wearableSubElement.Name.LocalName.ToLowerInvariant()) + { + case "damagemodifier": + wearableDamageModifiers.Add(new DamageModifier(wearableSubElement, Name.Value + ", Wearable", checkErrors: false)); + break; + case "skillmodifier": + Identifier skillIdentifier = wearableSubElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); + float skillValue = wearableSubElement.GetAttributeFloat("skillvalue", 0f); + if (wearableSkillModifiers.ContainsKey(skillIdentifier)) + { + wearableSkillModifiers[skillIdentifier] += skillValue; + } + else + { + wearableSkillModifiers.TryAdd(skillIdentifier, skillValue); + } + break; + } + } + break; } } + this.wearableDamageModifiers = wearableDamageModifiers.ToImmutableList(); + this.wearableSkillModifiers = wearableSkillModifiers.ToImmutableDictionary(); UpgradeOverrideSprites = upgradeOverrideSprites.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); BrokenSprites = brokenSprites.ToImmutableArray(); @@ -208,6 +238,26 @@ namespace Barotrauma DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); } + public bool CanCharacterBuy() + { + if (DefaultPrice == null) { return false; } + if (!DefaultPrice.RequiresUnlock) { return true; } + return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); + } + public LocalizedString GetTooltip() + { + LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; + if (!Description.IsNullOrEmpty()) + { + tooltip += $"\n{Description}"; + } + if (wearableDamageModifiers.Any() || wearableSkillModifiers.Any()) + { + Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); + } + return tooltip; + } + public override void UpdatePlacing(Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); @@ -313,15 +363,7 @@ namespace Barotrauma } else { - Vector2 position = Submarine.MouseToWorldGrid(Screen.Selected.Cam, Submarine.MainSub); - Vector2 placeSize = Size * Scale; - if (placePosition != Vector2.Zero) - { - if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } - if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } - position = placePosition; - } - Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs index b7aae9022..7068a7ed6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs @@ -168,11 +168,11 @@ namespace Barotrauma } else if (position.X < 0.0f) { - obstacleDiff = Vector2.UnitX; + obstacleDiff = -Vector2.UnitX; } else if (position.X > Level.Loaded.Size.X) { - obstacleDiff = -Vector2.UnitX; + obstacleDiff = Vector2.UnitX; } else { @@ -183,7 +183,7 @@ namespace Barotrauma foreach (Voronoi2.VoronoiCell cell in cells) { Vector2 diff = cell.Center - position; - if (diff.LengthSquared() > 5000.0f * 5000.0f) continue; + if (diff.LengthSquared() > 5000.0f * 5000.0f) { continue; } obstacleDiff += diff; cellCount++; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index ce1348882..f5fc36a94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -226,7 +226,7 @@ namespace Barotrauma { if (SoundChannels[i] == null || !SoundChannels[i].IsPlaying) { - SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, soundPos); + SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, roundSound.GetRandomFrequencyMultiplier(), soundPos); } SoundChannels[i].Position = new Vector3(soundPos.X, soundPos.Y, 0.0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index bdf919218..c123c22f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -178,7 +179,7 @@ namespace Barotrauma activeSprite?.Draw( spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 3000.0f), + Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 3000.0f), activeSprite.Origin, obj.CurrentRotation, obj.CurrentScale, @@ -200,7 +201,7 @@ namespace Barotrauma obj.ActivePrefab.DeformableSprite.Origin, obj.CurrentRotation, obj.CurrentScale, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 5000.0f)); + Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index ca5456156..e3aec5d91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -343,38 +343,31 @@ namespace Barotrauma.Lights { SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); - foreach (Character character in Character.CharacterList) - { - if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } - if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : - character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); - foreach (Limb limb in character.AnimController.Limbs) - { - if (limb.DeformSprite != null) { continue; } - limb.Draw(spriteBatch, cam, lightColor); - } - } + DrawCharacters(spriteBatch, cam, drawDeformSprites: false); spriteBatch.End(); DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); + DrawCharacters(spriteBatch, cam, drawDeformSprites: true); + spriteBatch.End(); + } + + static void DrawCharacters(SpriteBatch spriteBatch, Camera cam, bool drawDeformSprites) + { foreach (Character character in Character.CharacterList) { - if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible || character.InvisibleTimer > 0.0f) { continue; } if (Character.Controlled?.FocusedCharacter == character) { continue; } Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? Color.Black : character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.DeformSprite == null) { continue; } + if (drawDeformSprites == (limb.DeformSprite == null)) { continue; } limb.Draw(spriteBatch, cam, lightColor); } } - spriteBatch.End(); } DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index c32f82187..8339eb987 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -291,7 +291,7 @@ namespace Barotrauma if (!currentDisplayLocation.Discovered) { RemoveFogOfWar(currentDisplayLocation); - currentDisplayLocation.Discover(); + Discover(currentDisplayLocation); if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = currentDisplayLocation; @@ -452,7 +452,7 @@ namespace Barotrauma Level.Loaded.DebugSetStartLocation(CurrentLocation); Level.Loaded.DebugSetEndLocation(null); - CurrentLocation.Discover(); + Discover(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) @@ -693,7 +693,27 @@ namespace Barotrauma pos.Y = (int)pos.Y; Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); - Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); + Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); + Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y); + + int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); + List<(SubmarineClass subClass, int tier)> overrideTiers = null; + if (HighlightedLocation.CanHaveSubsForSale()) + { + overrideTiers = new List<(SubmarineClass subClass, int tier)>(); + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) + { + if (subClass == SubmarineClass.Undefined) { continue; } + int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); + if (highestClassTier > 0 && highestClassTier > highestSubTier) + { + overrideTiers.Add((subClass, highestClassTier)); + } + } + } + int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0); + size.Y += subAvailabilityTextCount * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; + bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; LocalizedString repLabelText = null, repValueText = null; Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; @@ -706,21 +726,54 @@ namespace Barotrauma repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); } + GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, new Rectangle((int)(pos.X - 60 * GUI.Scale), (int)(pos.Y - size.Y), (int)(size.X + 120 * GUI.Scale), (int)(size.Y * 2.2f)), Color.Black * hudVisibility); + spriteBatch, + new Rectangle( + (int)(pos.X - 60 * GUI.Scale), + (int)(pos.Y - size.Y), + (int)(size.X + 120 * GUI.Scale), + (int)(size.Y * 2.2f)), + Color.Black * hudVisibility); + var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); topLeftPos += new Vector2(0.0f, nameSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + DrawText(HighlightedLocation.Type.Name); + if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) + { + topLeftPos += new Vector2(0.0f, descSize.Y); + DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); + } + + if (highestSubTier > 0) + { + DrawSubAvailabilityText("advancedsub.all", highestSubTier); + } + if (overrideTiers != null) + { + foreach (var (subClass, tier) in overrideTiers) + { + DrawSubAvailabilityText($"advancedsub.{subClass}", tier); + } + } + void DrawSubAvailabilityText(string tag, int tier) + { + topLeftPos += new Vector2(0.0f, typeSize.Y); + DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); + } + if (showReputation) { topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, repLabelText.Value, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + DrawText(repLabelText.Value); topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); } + + void DrawText(LocalizedString text, GUIFont font = null) => GUI.DrawString(spriteBatch, topLeftPos, text, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: font); } if (drawRadiationTooltip) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index a33dfa34a..2ebbfc897 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -1042,19 +1042,11 @@ namespace Barotrauma protected static void PositionEditingHUD() { - int maxHeight = 100; - if (Screen.Selected == GameMain.SubEditorScreen) - { - editingHUD.RectTransform.SetPosition(Anchor.TopRight); - editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom); - maxHeight = (GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height) - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20; - } - else - { - editingHUD.RectTransform.SetPosition(Anchor.TopRight); - editingHUD.RectTransform.RelativeOffset = new Vector2(0.0f, (HUDLayoutSettings.CrewArea.Bottom + 10.0f) / (editingHUD.RectTransform.Parent ?? GUI.Canvas).Rect.Height); - maxHeight = HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10; - } + int maxHeight = + Screen.Selected == GameMain.SubEditorScreen ? + GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20 : + HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10; + var listBox = editingHUD.GetChild(); if (listBox != null) @@ -1074,6 +1066,17 @@ namespace Barotrauma MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false); listBox.RectTransform.Resize(new Point(listBox.RectTransform.NonScaledSize.X, editingHUD.RectTransform.NonScaledSize.Y - padding * 2), resizeChildren: false); } + editingHUD.RectTransform.SetPosition(Anchor.TopRight); + if (Screen.Selected == GameMain.SubEditorScreen) + { + editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom); + } + else + { + editingHUD.RectTransform.AbsoluteOffset = new Point( + 0, + HUDLayoutSettings.HealthBarAfflictionArea.Y - editingHUD.Rect.Height - GUI.IntScale(10)); + } } public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 8c1319fe5..4ae286687 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -6,9 +6,9 @@ namespace Barotrauma.Networking { partial class ChatMessage { - public virtual void ClientWrite(IWriteMessage msg) + public virtual void ClientWrite(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { - msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); + segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index f235624b7..89abf5bf8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,12 +1,15 @@ using System.Diagnostics; using System.IO.Pipes; using System.Linq; +using System.Threading; namespace Barotrauma.Networking { static partial class ChildServerRelay { public static Process Process; + public static bool IsProcessAlive => Process is { HasExited: false }; + private static bool localHandlesDisposed; private static AnonymousPipeServerStream writePipe; private static AnonymousPipeServerStream readPipe; @@ -44,18 +47,27 @@ namespace Barotrauma.Networking localHandlesDisposed = true; } - public static void ClosePipes() + public static void AttemptGracefulShutDown(int maxAttempts = 20) { - writePipe?.Dispose(); writePipe = null; - readPipe?.Dispose(); readPipe = null; - shutDown = true; + status = StatusEnum.RequestedShutDown; + writeManualResetEvent?.Set(); + int checks = 0; + while (Process is { HasExited: false }) + { + if (checks >= maxAttempts) + { + DebugConsole.AddWarning("Server could not be shut down gracefully"); + break; + } + Thread.Sleep(100); + checks++; + } + ForceShutDown(); } - - public static void ShutDown() + + public static void ForceShutDown() { Process?.Kill(); Process = null; - writePipe = null; readPipe = null; - PrivateShutDown(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 21b119c40..460d06df1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -14,6 +14,16 @@ namespace Barotrauma.Networking set; } + private SoundChannel radioNoiseChannel; + private float radioNoise; + + public float RadioNoise + { + get { return radioNoise; } + set { radioNoise = MathHelper.Clamp(value, 0.0f, 1.0f); } + } + + private bool mutedLocally; public bool MutedLocally { @@ -42,35 +52,64 @@ namespace Barotrauma.Networking !HasPermission(ClientPermissions.Kick) && !HasPermission(ClientPermissions.Unban); - public void UpdateSoundPosition() + public void UpdateVoipSound() { - if (VoipSound == null) { return; } - - if (!VoipSound.IsPlaying) + if (VoipSound == null || !VoipSound.IsPlaying) { - DebugConsole.Log("Destroying voipsound"); - VoipSound.Dispose(); + radioNoiseChannel?.Dispose(); + radioNoiseChannel = null; + if (VoipSound != null) + { + DebugConsole.Log("Destroying voipsound"); + VoipSound.Dispose(); + } VoipSound = null; - return; + return; } + if (Screen.Selected is ModDownloadScreen) + { + VoipSound.Gain = 0.0f; + } + + float gain = 1.0f; + float noiseGain = 0.0f; + Vector3? position = null; if (character != null) { if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) { - VoipSound.SetPosition(new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f)); + position = new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f); } else { - VoipSound.SetPosition(null); float dist = Vector3.Distance(new Vector3(character.WorldPosition, 0.0f), GameMain.SoundManager.ListenerPosition); - VoipSound.Gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist); + gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist); + } + if (RadioNoise > 0.0f) + { + noiseGain = gain * RadioNoise; + gain *= 1.0f - RadioNoise; } } - else + VoipSound.SetPosition(position); + VoipSound.Gain = gain; + if (noiseGain > 0.0f) { - VoipSound.SetPosition(null); - VoipSound.Gain = 1.0f; + if (radioNoiseChannel == null || !radioNoiseChannel.IsPlaying) + { + radioNoiseChannel = SoundPlayer.PlaySound("radiostatic"); + radioNoiseChannel.Category = "voip"; + radioNoiseChannel.Looping = true; + } + radioNoiseChannel.Near = VoipSound.Near; + radioNoiseChannel.Far = VoipSound.Far; + radioNoiseChannel.Position = position; + radioNoiseChannel.Gain = noiseGain; + } + else if (radioNoiseChannel != null) + { + radioNoiseChannel.Gain = 0.0f; } } @@ -158,6 +197,11 @@ namespace Barotrauma.Networking VoipSound.Dispose(); VoipSound = null; } + if (radioNoiseChannel != null) + { + radioNoiseChannel.Dispose(); + radioNoiseChannel = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 8a61e624a..453fb3cb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -311,6 +311,12 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } + public void SetLobbyPublic(bool isPublic) + { + GameMain.NetLobbyScreen.SetPublic(isPublic); + SteamManager.SetLobbyPublic(isPublic); + } + private ClientPeer CreateNetPeer() { Networking.ClientPeer.Callbacks callbacks = new ClientPeer.Callbacks( @@ -326,10 +332,27 @@ namespace Barotrauma.Networking }; } + public void CreateServerCrashMessage() + { + // Close any message boxes that say "The server has crashed." + var basicServerCrashMsg = TextManager.Get($"{nameof(DisconnectReason)}.{nameof(DisconnectReason.ServerCrashed)}"); + GUIMessageBox.MessageBoxes + .OfType() + .Where(mb => mb.Text?.Text == basicServerCrashMsg) + .ToArray() + .ForEach(mb => mb.Close()); + + // Open a new message box with the crash report path + if (GUIMessageBox.MessageBoxes.All( + mb => (mb as GUIMessageBox)?.Text?.Text != ChildServerRelay.CrashMessage)) + { + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); + msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; + } + } + private bool ReturnToPreviousMenu(GUIButton button, object obj) { - Quit(); - Submarine.Unload(); GameMain.Client = null; GameMain.GameSession = null; @@ -443,7 +466,7 @@ namespace Barotrauma.Networking foreach (Client c in ConnectedClients) { if (c.Character != null && c.Character.Removed) { c.Character = null; } - c.UpdateSoundPosition(); + c.UpdateVoipSound(); } if (VoipCapture.Instance != null) @@ -479,15 +502,11 @@ namespace Barotrauma.Networking } catch (Exception e) { - string errorMsg = "Error while reading a message from server. {" + e + "}. "; + string errorMsg = "Error while reading a message from server. "; if (GameMain.Client == null) { errorMsg += "Client disposed."; } - errorMsg += "\n" + e.StackTrace.CleanupStackTrace(); - if (e.InnerException != null) - { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); - } + AppendExceptionInfo(ref errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - DebugConsole.ThrowError("Error while reading a message from server.", e); + DebugConsole.ThrowError(errorMsg); new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))) { DisplayInLoadingScreens = true @@ -529,14 +548,10 @@ namespace Barotrauma.Networking { if (GameMain.WindowActive) { - if (ChildServerRelay.Process?.HasExited ?? true) + if (!ChildServerRelay.IsProcessAlive) { Quit(); - if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage)) - { - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - } + CreateServerCrashMessage(); } } } @@ -632,14 +647,8 @@ namespace Barotrauma.Networking } catch (Exception e) { - string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - if (e.InnerException != null) - { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); - } -#if DEBUG - DebugConsole.ThrowError("Error while reading an ingame update message from server.", e); -#endif + string errorMsg = "Error while reading an ingame update message from server."; + AppendExceptionInfo(ref errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw; } @@ -868,22 +877,31 @@ namespace Barotrauma.Networking } byte missionCount = inc.ReadByte(); - if (missionCount != GameMain.GameSession.Missions.Count()) - { - string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; - throw new Exception(errorMsg); - } List serverMissionIdentifiers = new List(); for (int i = 0; i < missionCount; i++) { serverMissionIdentifiers.Add(inc.ReadIdentifier()); } + if (missionCount != GameMain.GameSession.GameMode.Missions.Count()) + { + string errorMsg = + $"Mission equality check failed. Mission count doesn't match the server. " + + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } if (missionCount > 0) { - if (!GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id))) + if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id))) { - string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {string.Join(", ", serverMissionIdentifiers)}, client: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + string errorMsg = + $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " + + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -939,13 +957,6 @@ namespace Barotrauma.Networking GUI.ClearCursorWait(); - ChildServerRelay.ShutDown(); - - if (SteamManager.IsInitialized) - { - Steamworks.SteamFriends.ClearRichPresence(); - } - if (disconnectPacket.ShouldCreateAnalyticsEvent) { GameAnalyticsManager.AddErrorEventOnce( @@ -971,11 +982,43 @@ namespace Barotrauma.Networking } else { - ReturnToPreviousMenu(null, null); - new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage) + if (ClientPeer is SteamP2PClientPeer or SteamP2POwnerPeer) { - DisplayInLoadingScreens = true - }; + SteamManager.LeaveLobby(); + } + + GameMain.ModDownloadScreen.Reset(); + ContentPackageManager.EnabledPackages.Restore(); + + CampaignMode.StartRoundCancellationToken?.Cancel(); + + if (SteamManager.IsInitialized) + { + Steamworks.SteamFriends.ClearRichPresence(); + } + foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) + { + FileReceiver.StopTransfer(fileTransfer, deleteFile: true); + } + + ChildServerRelay.AttemptGracefulShutDown(); + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); + + characterInfo?.Remove(); + + VoipClient?.Dispose(); + VoipClient = null; + GameMain.Client = null; + GameMain.GameSession = null; + + ReturnToPreviousMenu(null, null); + if (disconnectPacket.DisconnectReason != DisconnectReason.Disconnected) + { + new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage) + { + DisplayInLoadingScreens = true + }; + } } } @@ -1321,6 +1364,7 @@ namespace Barotrauma.Networking ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); + ServerSettings.ShowEnemyHealthBars = (EnemyHealthBarMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; @@ -1878,12 +1922,11 @@ namespace Barotrauma.Networking private void ReadLobbyUpdate(IReadMessage inc) { - ServerNetObject objHeader; - while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) + SegmentTableReader.Read(inc, (segment, inc) => { - switch (objHeader) + switch (segment) { - case ServerNetObject.SYNC_IDS: + case ServerNetSegment.SyncIds: bool lobbyUpdated = inc.ReadBoolean(); inc.ReadPadBits(); @@ -2015,17 +2058,19 @@ namespace Barotrauma.Networking lastSentChatMsgID = inc.ReadUInt16(); break; - case ServerNetObject.CLIENT_LIST: + case ServerNetSegment.ClientList: ReadClientList(inc); break; - case ServerNetObject.CHAT_MESSAGE: + case ServerNetSegment.ChatMessage: ChatMessage.ClientRead(inc); break; - case ServerNetObject.VOTE: + case ServerNetSegment.Vote: Voting.ClientRead(inc); break; } - } + + return SegmentTableReader.BreakSegmentReading.No; + }); } readonly List debugEntityList = new List(); @@ -2035,117 +2080,106 @@ namespace Barotrauma.Networking float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; - ServerNetObject? prevObjHeader = null; - long prevBitPos = 0; - long prevBytePos = 0; - - long prevBitLength = 0; - long prevByteLength = 0; - - ServerNetObject? objHeader = null; - try + SegmentTableReader.Read(inc, + segmentDataReader: (segment, inc) => { - while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) + switch (segment) { - switch (objHeader) - { - case ServerNetObject.SYNC_IDS: - lastSentChatMsgID = inc.ReadUInt16(); - LastSentEntityEventID = inc.ReadUInt16(); + case ServerNetSegment.SyncIds: + lastSentChatMsgID = inc.ReadUInt16(); + LastSentEntityEventID = inc.ReadUInt16(); - bool campaignUpdated = inc.ReadBoolean(); - inc.ReadPadBits(); - if (campaignUpdated) - { - MultiPlayerCampaign.ClientRead(inc); - } - else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) - { - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); - } - break; - case ServerNetObject.ENTITY_POSITION: - inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - - bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); - UInt32 incomingUintIdentifier = inc.ReadUInt32(); - UInt16 id = inc.ReadUInt16(); - uint msgLength = inc.ReadVariableUInt32(); - int msgEndPos = (int)(inc.BitPosition + msgLength * 8); + bool campaignUpdated = inc.ReadBoolean(); + inc.ReadPadBits(); + if (campaignUpdated) + { + MultiPlayerCampaign.ClientRead(inc); + } + else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) + { + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + } + break; + case ServerNetSegment.EntityPosition: + inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + + bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); + UInt32 incomingUintIdentifier = inc.ReadUInt32(); + UInt16 id = inc.ReadUInt16(); + uint msgLength = inc.ReadVariableUInt32(); + int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - var entity = Entity.FindEntityByID(id) as IServerPositionSync; - if (msgEndPos > inc.LengthBits) - { - DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); - return; - } + var entity = Entity.FindEntityByID(id) as IServerPositionSync; + if (msgEndPos > inc.LengthBits) + { + DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); + return SegmentTableReader.BreakSegmentReading.Yes; + } - debugEntityList.Add(entity); - if (entity != null) + debugEntityList.Add(entity); + if (entity != null) + { + if (entity is Item != isItem) { - if (entity is Item != isItem) - { - DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); - } - else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && - uintIdentifier != incomingUintIdentifier) - { - DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." - +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " - +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); - } - else - { - entity.ClientReadPosition(inc, sendingTime); - } + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); } - - //force to the correct position in case the entity doesn't exist - //or the message wasn't read correctly for whatever reason - inc.BitPosition = msgEndPos; - inc.ReadPadBits(); - break; - case ServerNetObject.CLIENT_LIST: - ReadClientList(inc); - break; - case ServerNetObject.ENTITY_EVENT: - case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) + else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && + uintIdentifier != incomingUintIdentifier) { - return; + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." + +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " + +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); } - break; - case ServerNetObject.CHAT_MESSAGE: - ChatMessage.ClientRead(inc); - break; - default: - throw new Exception($"Unknown object header \"{objHeader}\"!)"); - } - prevBitLength = inc.BitPosition - prevBitPos; - prevByteLength = inc.BytePosition - prevBytePos; + else + { + entity.ClientReadPosition(inc, sendingTime); + } + } - prevObjHeader = objHeader; - prevBitPos = inc.BitPosition; - prevBytePos = inc.BytePosition; + //force to the correct position in case the entity doesn't exist + //or the message wasn't read correctly for whatever reason + inc.BitPosition = msgEndPos; + inc.ReadPadBits(); + break; + case ServerNetSegment.ClientList: + ReadClientList(inc); + break; + case ServerNetSegment.EntityEvent: + case ServerNetSegment.EntityEventInitial: + if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList)) + { + return SegmentTableReader.BreakSegmentReading.Yes; + } + break; + case ServerNetSegment.ChatMessage: + ChatMessage.ClientRead(inc); + break; + default: + throw new Exception($"Unknown segment \"{segment}\"!)"); } - } - catch (Exception ex) + + return SegmentTableReader.BreakSegmentReading.No; + }, + exceptionHandler: (segment, prevSegments, ex) => { List errorLines = new List { ex.Message, "Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)", "Read position: " + inc.BitPosition, - "Header: " + (objHeader != null ? objHeader.Value.ToString() : "Error occurred on the very first header!"), - prevObjHeader != null ? "Previous header: " + prevObjHeader : "Error occurred on the very first header!", - "Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)", - " " + $"Segment with error: {segment}" }; + if (prevSegments.Any()) + { + errorLines.Add("Prev segments: " + string.Join(", ", prevSegments)); + errorLines.Add(" "); + } errorLines.Add(ex.StackTrace.CleanupStackTrace()); errorLines.Add(" "); - if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL || - objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL || - objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION) + if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier + is ServerNetSegment.EntityPosition + or ServerNetSegment.EntityEvent + or ServerNetSegment.EntityEventInitial)) { foreach (IServerSerializable ent in debugEntityList) { @@ -2159,34 +2193,18 @@ namespace Barotrauma.Networking } } - foreach (string line in errorLines) - { - DebugConsole.ThrowError(line); - } errorLines.Add("Last console messages:"); for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--) { errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text); } GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines)); - - DebugConsole.ThrowError("Writing object data to \"networkerror_data.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); - - using (FileStream fl = File.Open("networkerror_data.log", System.IO.FileMode.Create)) - { - using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fl)) - using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fl)) - { - bw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); - sw.WriteLine(""); - foreach (string line in errorLines) - { - sw.WriteLine(line); - } - } - } - throw new Exception("Read error: please send us \"networkerror_data.log\"!"); - } + + throw new Exception( + $"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." + + (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""), + ex); + }); } private void SendLobbyUpdate() @@ -2194,50 +2212,51 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); - outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); - outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); - outmsg.WriteUInt16(ChatMessage.LastID); - outmsg.WriteUInt16(LastClientListUpdateID); - outmsg.WriteUInt16(nameId); - outmsg.WriteString(Name); - var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; - if (jobPreferences.Count > 0) + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier); - } - else - { - outmsg.WriteIdentifier(Identifier.Empty); - } - outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); - - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) - { - outmsg.WriteUInt16((UInt16)0); - } - else - { - outmsg.WriteUInt16(campaign.LastSaveID); - outmsg.WriteByte(campaign.CampaignID); - foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + segmentTable.StartNewSegment(ClientNetSegment.SyncIds); + outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + outmsg.WriteUInt16(ChatMessage.LastID); + outmsg.WriteUInt16(LastClientListUpdateID); + outmsg.WriteUInt16(nameId); + outmsg.WriteString(Name); + var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; + if (jobPreferences.Count > 0) { - outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag)); + outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier); } - outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); - } - - chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); - for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) - { - if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + else { - //no more room in this packet - break; + outmsg.WriteIdentifier(Identifier.Empty); } - chatMsgQueue[i].ClientWrite(outmsg); - } - outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) + { + outmsg.WriteUInt16((UInt16)0); + } + else + { + outmsg.WriteUInt16(campaign.LastSaveID); + outmsg.WriteByte(campaign.CampaignID); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag)); + } + outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + } + + chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); + for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) + { + if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + { + //no more room in this packet + break; + } + chatMsgQueue[i].ClientWrite(segmentTable, outmsg); + } + } if (outmsg.LengthBytes > MsgConstants.MTU) { DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); @@ -2253,44 +2272,47 @@ namespace Barotrauma.Networking outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone); outmsg.WritePadBits(); - outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); - //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); - outmsg.WriteUInt16(ChatMessage.LastID); - outmsg.WriteUInt16(EntityEventManager.LastReceivedID); - outmsg.WriteUInt16(LastClientListUpdateID); + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) + { + segmentTable.StartNewSegment(ClientNetSegment.SyncIds); + //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); + outmsg.WriteUInt16(ChatMessage.LastID); + outmsg.WriteUInt16(EntityEventManager.LastReceivedID); + outmsg.WriteUInt16(LastClientListUpdateID); - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) - { - outmsg.WriteUInt16((UInt16)0); - } - else - { - outmsg.WriteUInt16(campaign.LastSaveID); - outmsg.WriteByte(campaign.CampaignID); - foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { - outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag)); + outmsg.WriteUInt16((UInt16)0); } - outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); - } - - Character.Controlled?.ClientWriteInput(outmsg); - GameMain.GameScreen.Cam?.ClientWrite(outmsg); - - EntityEventManager.Write(outmsg, ClientPeer?.ServerConnection); - - chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); - for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) - { - if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + else { - //not enough room in this packet - break; - } - chatMsgQueue[i].ClientWrite(outmsg); - } + outmsg.WriteUInt16(campaign.LastSaveID); + outmsg.WriteByte(campaign.CampaignID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag)); + } - outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE); + outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + } + + Character.Controlled?.ClientWriteInput(segmentTable, outmsg); + GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg); + + EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection); + + chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); + for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) + { + if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + { + //not enough room in this packet + break; + } + + chatMsgQueue[i].ClientWrite(segmentTable, outmsg); + } + } if (outmsg.LengthBytes > MsgConstants.MTU) { @@ -2523,7 +2545,7 @@ namespace Barotrauma.Networking public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { - if (!(entity is IClientSerializable clientSerializable)) + if (entity is not IClientSerializable clientSerializable) { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } @@ -2557,46 +2579,10 @@ namespace Barotrauma.Networking public void Quit() { GameMain.LuaCs.Stop(); - if (ClientPeer is SteamP2PClientPeer || ClientPeer is SteamP2POwnerPeer) - { - SteamManager.LeaveLobby(); - } - - GameMain.ModDownloadScreen.Reset(); - ContentPackageManager.EnabledPackages.Restore(); - - CampaignMode.StartRoundCancellationToken?.Cancel(); - + ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); - ClientPeer = null; - - foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) - { - FileReceiver.StopTransfer(fileTransfer, deleteFile: true); - } - - if (ChildServerRelay.Process != null) - { - int checks = 0; - while (ChildServerRelay.Process is { HasExited: false }) - { - if (checks > 10) - { - ChildServerRelay.ShutDown(); - } - Thread.Sleep(100); - checks++; - } - } - ChildServerRelay.ShutDown(); + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); - - characterInfo?.Remove(); - - VoipClient?.Dispose(); - VoipClient = null; - GameMain.Client = null; - GameMain.GameSession = null; } public void SendCharacterInfo(string newName = null) @@ -2604,7 +2590,6 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); WriteCharacterInfo(msg, newName); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer?.Send(msg, DeliveryMethod.Reliable); } @@ -2644,9 +2629,11 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); - msg.WriteByte((byte)ClientNetObject.VOTE); - Voting.ClientWrite(msg, voteType, data); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + using (var segmentTable = SegmentTableWriter.StartWriting(msg)) + { + segmentTable.StartNewSegment(ClientNetSegment.Vote); + Voting.ClientWrite(msg, voteType, data); + } ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2762,7 +2749,6 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2811,7 +2797,6 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)ClientPermissions.SelectSub); msg.WriteBoolean(isShuttle); msg.WritePadBits(); msg.WriteString(sub.MD5Hash.StringRepresentation); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2831,7 +2816,6 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.SelectMode); msg.WriteUInt16((UInt16)modeIndex); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3537,6 +3521,23 @@ namespace Barotrauma.Networking eventErrorWritten = true; } + private static void AppendExceptionInfo(ref string errorMsg, Exception e) + { + if (!errorMsg.EndsWith("\n")) { errorMsg += "\n"; } + errorMsg += e.Message + "\n"; + var innermostException = e.GetInnermost(); + if (innermostException != e) + { + // If available, only append the stacktrace of the innermost exception, + // because that's the most important one to fix + errorMsg += "Inner exception: " + innermostException.Message + "\n" + innermostException.StackTrace.CleanupStackTrace(); + } + else + { + errorMsg += e.StackTrace.CleanupStackTrace(); + } + } + #if DEBUG public void ForceTimeOut() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 4a46f3c9e..0d14de93b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -66,7 +66,7 @@ namespace Barotrauma.Networking events.Add(newEvent); } - public void Write(IWriteMessage msg, NetworkConnection serverConnection) + public void Write(in SegmentTableWriter segmentTable, IWriteMessage msg, NetworkConnection serverConnection) { if (events.Count == 0 || serverConnection == null) return; @@ -103,7 +103,7 @@ namespace Barotrauma.Networking eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now; } - msg.WriteByte((byte)ClientNetObject.ENTITY_STATE); + segmentTable.StartNewSegment(ClientNetSegment.EntityState); Write(msg, eventsToSync, out _); } @@ -112,11 +112,11 @@ namespace Barotrauma.Networking /// /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// - public bool Read(ServerNetObject type, IReadMessage msg, float sendingTime, List entities) + public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List entities) { UInt16 unreceivedEntityEventCount = 0; - if (type == ServerNetObject.ENTITY_EVENT_INITIAL) + if (type == ServerNetSegment.EntityEventInitial) { unreceivedEntityEventCount = msg.ReadUInt16(); firstNewID = msg.ReadUInt16(); @@ -218,43 +218,20 @@ namespace Barotrauma.Networking Microsoft.Xna.Framework.Color.Green); } lastReceivedID++; - try + ReadEvent(msg, entity, sendingTime); + msg.ReadPadBits(); + + if (msg.BitPosition != msgPosition + msgLength * 8) { - ReadEvent(msg, entity, sendingTime); - msg.ReadPadBits(); + var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; + ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; + string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " + +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " + +$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits."; - if (msg.BitPosition != msgPosition + msgLength * 8) - { - var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; - ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; - string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " - +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " - +$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits."; - - DebugConsole.ThrowError(errorMsg); - - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - - //TODO: force the BitPosition to correct place? Having some entity in a potentially incorrect state is not as bad as a desync kick - //msg.BitPosition = (int)(msgPosition + msgLength * 8); - } - } - catch (Exception e) - { - string errorMsg = $"Failed to read event {thisEventID} for entity \"{entity}\"" + - $"{(entity is Entity { ID: var entityId } ? $", id {entityId}" : "")} "; - DebugConsole.ThrowError(errorMsg, e); - - errorMsg += $"({e.Message})! (MidRoundSyncing: {thisClient.MidRoundSyncing})\n{e.StackTrace.CleanupStackTrace()}"; - errorMsg += "\nPrevious entities:"; - for (int j = entities.Count - 2; j >= 0; j--) - { - errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); - } - - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), - GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - msg.BitPosition = (int)(msgPosition + msgLength * 8); + GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + + throw new Exception(errorMsg); } } } @@ -277,16 +254,12 @@ namespace Barotrauma.Networking public void Clear() { - ID = 0; - lastReceivedID = 0; - firstNewID = null; - - events.Clear(); eventLastSent.Clear(); - MidRoundSyncingDone = false; + + ClearSelf(); } /// @@ -297,6 +270,10 @@ namespace Barotrauma.Networking { ID = 0; events.Clear(); + if (thisClient != null) + { + thisClient.LastSentEntityEventID = 0; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index b21592b8d..9d7c0df0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -4,9 +4,9 @@ namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { - public override void ClientWrite(IWriteMessage msg) + public override void ClientWrite(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { - msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); + segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index a95afd553..8e599ee17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -91,15 +91,11 @@ namespace Barotrauma.Networking ToolBox.ThrowIfNull(netClient); ToolBox.ThrowIfNull(incomingLidgrenMessages); - if (isOwner && !(ChildServerRelay.Process is { HasExited: false })) + if (isOwner && !ChildServerRelay.IsProcessAlive) { + var gameClient = GameMain.Client; Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += (btn, obj) => - { - GameMain.MainMenuScreen.Select(); - return false; - }; + gameClient?.CreateServerCrashMessage(); return; } @@ -111,7 +107,7 @@ namespace Barotrauma.Networking foreach (NetIncomingMessage inc in incomingLidgrenMessages) { - if (!inc.SenderConnection.RemoteEndPoint.Equals(lidgrenEndpoint.NetEndpoint)) + if (!inc.SenderConnection.RemoteEndPoint.EquivalentTo(lidgrenEndpoint.NetEndpoint)) { DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}"); continue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 48fe24b0e..54c4d7ca2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -187,15 +187,11 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false })) + if (ChildServerRelay.HasShutDown || !ChildServerRelay.IsProcessAlive) { + var gameClient = GameMain.Client; Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += (btn, obj) => - { - GameMain.MainMenuScreen.Select(); - return false; - }; + gameClient?.CreateServerCrashMessage(); return; } @@ -401,8 +397,6 @@ namespace Barotrauma.Networking ClosePeerSession(remotePeers[i]); } - ChildServerRelay.ClosePipes(); - callbacks.OnDisconnect.Invoke(peerDisconnectPacket); SteamManager.LeaveLobby(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs index fc23c4df9..56138ec83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Networking { static class PingUtils { - private static readonly Dictionary activePings = new Dictionary(); + private static readonly Dictionary activePings = new Dictionary(); private static bool steamPingInfoReady; @@ -36,9 +36,9 @@ namespace Barotrauma.Networking switch (serverInfo.Endpoint) { - case LidgrenEndpoint { NetEndpoint: { Address: var address } }: + case LidgrenEndpoint { NetEndpoint: var endPoint }: - GetIPAddressPing(serverInfo, address, onPingDiscovered); + GetIPAddressPing(serverInfo, endPoint, onPingDiscovered); break; case SteamP2PEndpoint steamP2PEndpoint: TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})", @@ -131,9 +131,9 @@ namespace Barotrauma.Networking } } - private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action onPingDiscovered) + private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action onPingDiscovered) { - if (IPAddress.IsLoopback(address)) + if (IPAddress.IsLoopback(endPoint.Address)) { serverInfo.Ping = Option.Some(0); onPingDiscovered(serverInfo); @@ -142,24 +142,24 @@ namespace Barotrauma.Networking { lock (activePings) { - if (activePings.ContainsKey(address)) { return; } - activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0); + if (activePings.ContainsKey(endPoint)) { return; } + activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0); } serverInfo.Ping = Option.None(); - TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), + TaskPool.Add($"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000), rtt => { if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option.None(); } onPingDiscovered(serverInfo); lock (activePings) { - activePings.Remove(address); + activePings.Remove(endPoint); } }); } } - private static async Task> PingServerAsync(IPAddress ipAddress, int timeOut) + private static async Task> PingServerAsync(IPEndPoint endPoint, int timeOut) { await Task.Yield(); bool shouldGo = false; @@ -167,21 +167,21 @@ namespace Barotrauma.Networking { lock (activePings) { - shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25; + shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25; } await Task.Delay(25); } - if (ipAddress == null) { return Option.None(); } + if (endPoint?.Address == null) { return Option.None(); } //don't attempt to ping if the address is IPv6 and it's not supported - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } + if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } Ping ping = new Ping(); byte[] buffer = new byte[32]; try { - PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true)); + PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer, new PingOptions(128, true)); return pingReply.Status switch { @@ -191,9 +191,9 @@ namespace Barotrauma.Networking } catch (Exception ex) { - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); + GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); #if DEBUG - DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); + DebugConsole.NewMessage("Failed to ping a server (" + endPoint.Address + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); #endif return Option.None(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index b6c1b2583..c6a97de44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Networking MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); - GameMain.NetLobbyScreen.SetPublic(IsPublic); + GameMain.Client?.SetLobbyPublic(IsPublic); AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); @@ -367,6 +367,17 @@ namespace Barotrauma.Networking //*********************************************** + //changing server visibility on the fly is not supported in dedicated servers + if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) + { + var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), + TextManager.Get("publicserver")) + { + ToolTip = TextManager.Get("publicservertooltip") + }; + GetPropertyData(nameof(IsPublic)).AssignGUIComponent(isPublic); + } + // Sub Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) @@ -475,9 +486,10 @@ namespace Barotrauma.Networking // game settings //-------------------------------------------------------------------------------- - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) { }; + var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)); + var roundsContent = new GUIListBox(new RectTransform(Vector2.One, roundsTab.RectTransform, Anchor.Center), style: "GUIListBoxNoBorder").Content; - GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsTab.RectTransform)); + GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsContent.RectTransform)); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playStyleLayout.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), playStyleLayout.RectTransform)) @@ -502,7 +514,7 @@ namespace Barotrauma.Networking GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); - GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsTab.RectTransform)) + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsContent.RectTransform)) { Stretch = true }; @@ -608,7 +620,7 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsTab.RectTransform)); + GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), losModeLayout.RectTransform), TextManager.Get("LosEffect")); @@ -629,7 +641,30 @@ namespace Barotrauma.Networking } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsTab.RectTransform)) + GUILayoutGroup healthBarModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); + + var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), healthBarModeLayout.RectTransform), + TextManager.Get("ShowEnemyHealthBars")); + + var healthBarModeRadioButtonLayout + = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), healthBarModeLayout.RectTransform), + isHorizontal: true) + { + Stretch = true + }; + + var healthBarModeRadioButtonGroup = new GUIRadioButtonGroup(); + EnemyHealthBarMode[] healthBarModeModes = Enum.GetValues(); + for (int i = 0; i < healthBarModeModes.Length; i++) + { + var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), healthBarModeRadioButtonLayout.RectTransform), + TextManager.Get($"ShowEnemyHealthBars.{healthBarModeModes[i]}"), + font: GUIStyle.SmallFont, style: "GUIRadioButton"); + healthBarModeRadioButtonGroup.AddRadioButton(i, losTick); + } + GetPropertyData(nameof(ShowEnemyHealthBars)).AssignGUIComponent(healthBarModeRadioButtonGroup); + + GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsContent.RectTransform)) { Stretch = true }; @@ -651,7 +686,7 @@ namespace Barotrauma.Networking var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), isHorizontal: true) + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsContent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index bfbba0c7f..cde031084 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -17,9 +17,7 @@ namespace Barotrauma.Networking get; private set; } - - public static IReadOnlyList CaptureDeviceNames => - Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + private readonly IntPtr captureDevice; @@ -169,6 +167,11 @@ namespace Barotrauma.Networking Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + public static IReadOnlyList GetCaptureDeviceNames() + { + return Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + } + IntPtr nativeBuffer; readonly short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; readonly short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; @@ -260,6 +263,13 @@ namespace Barotrauma.Networking } } } + + if (Screen.Selected is ModDownloadScreen) + { + allowEnqueue = false; + captureTimer = 0; + } + if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 934820e0d..7698c4b66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -1,20 +1,23 @@ -using Barotrauma.Sounds; +using Barotrauma.Items.Components; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Xna.Framework; -using Barotrauma.Items.Components; namespace Barotrauma.Networking { class VoipClient : IDisposable { - private GameClient gameClient; - private ClientPeer netClient; + /// + /// The "near" range of the voice chat (a percentage of either SpeakRange or radio range), further than this the volume starts to diminish + /// + const float RangeNear = 0.4f; + + private readonly GameClient gameClient; + private readonly ClientPeer netClient; private DateTime lastSendTime; - private List queues; + private readonly List queues; private UInt16 storedBufferID = 0; @@ -32,13 +35,13 @@ namespace Barotrauma.Networking public void RegisterQueue(VoipQueue queue) { - if (queue == VoipCapture.Instance) return; - if (!queues.Contains(queue)) queues.Add(queue); + if (queue == VoipCapture.Instance) { return; } + if (!queues.Contains(queue)) { queues.Add(queue); } } public void UnregisterQueue(VoipQueue queue) { - if (queues.Contains(queue)) queues.Remove(queue); + if (queues.Contains(queue)) { queues.Remove(queue); } } public void SendToServer() @@ -85,6 +88,7 @@ namespace Barotrauma.Networking public void Read(IReadMessage msg) { byte queueId = msg.ReadByte(); + float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8); VoipQueue queue = queues.Find(q => q.QueueID == queueId); if (queue == null) @@ -105,9 +109,12 @@ namespace Barotrauma.Networking client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); } GameMain.SoundManager.ForceStreamUpdate(); - + client.RadioNoise = 0.0f; if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) { + float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f; + bool spectating = Character.Controlled == null; + float rangeMultiplier = spectating ? 2.0f : 1.0f; WifiComponent radio = null; var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); @@ -115,11 +122,17 @@ namespace Barotrauma.Networking client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; if (messageType == ChatMessageType.Radio) { - client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); + client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier); + if (distanceFactor > RangeNear && !spectating) + { + //noise starts increasing exponentially after 40% range + client.RadioNoise = MathF.Pow(MathUtils.InverseLerp(RangeNear, 1.0f, distanceFactor), 2); + } } else { - client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange); + + client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier); } client.VoipSound.UseMuffleFilter = messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index cae3906e3..468f07e54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -82,6 +82,9 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool CopyEntityAngle { get; set; } + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")] + public bool CopyEntityDir { get; set; } + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 6efac8aeb..efd20b32b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -59,6 +59,7 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.None: + if (Key == Keys.None) { return false; } return PlayerInput.KeyDown(Key); case MouseButton.PrimaryMouse: return PlayerInput.PrimaryMouseButtonHeld(); @@ -88,6 +89,7 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.None: + if (Key == Keys.None) { return false; } return PlayerInput.KeyHit(Key); case MouseButton.PrimaryMouse: return PlayerInput.PrimaryMouseButtonClicked(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 436132dba..3da38f7df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -112,10 +112,23 @@ namespace Barotrauma } StringBuilder sb = new StringBuilder(); + sb.AppendLine("Barotrauma Client crash report (generated on " + DateTime.Now + ")"); - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); - sb.AppendLine("\n"); + sb.AppendLine(); + + string dxgiErrorHelpText = +#if WINDOWS + GetDXGIErrorHelpText(game, exception); +#else + string.Empty; +#endif + if (!string.IsNullOrEmpty(dxgiErrorHelpText)) + { + sb.AppendLine(dxgiErrorHelpText); + sb.AppendLine(); + } try { @@ -135,7 +148,7 @@ namespace Barotrauma XDocument newDoc = new XDocument(newElement); newDoc.Save(GameSettings.PlayerConfigPath); sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); - sb.AppendLine("\n"); + sb.AppendLine(); } } } @@ -148,7 +161,7 @@ namespace Barotrauma { sb.AppendLine(exeHash.StringRepresentation); } - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); sb.AppendLine($"Graphics mode: {GameSettings.CurrentConfig.Graphics.Width}x{GameSettings.CurrentConfig.Graphics.Height} ({GameSettings.CurrentConfig.Graphics.DisplayMode})"); @@ -171,7 +184,7 @@ namespace Barotrauma sb.AppendLine("Client (" + (GameMain.Client.GameStarted ? "Round had started)" : "Round hadn't been started)")); } - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("System info:"); sb.AppendLine(" Operating system: " + System.Environment.OSVersion + (System.Environment.Is64BitOperatingSystem ? " 64 bit" : " x86")); @@ -201,13 +214,14 @@ namespace Barotrauma } } - sb.AppendLine("\n"); - sb.AppendLine("Exception: " + exception.Message + " (" + exception.GetType().ToString() + ")"); + sb.AppendLine(); + sb.AppendLine($"Exception: {exception.Message} ({exception.GetType()})"); #if WINDOWS if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) { var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; - sb.AppendLine("Device removed reason: " + dxDevice.DeviceRemovedReason.ToString()); + var descriptor = ResultDescriptor.Find(dxDevice.DeviceRemovedReason)?.ApiCode ?? "UNKNOWN"; + sb.AppendLine($"Device removed reason: {descriptor} ({dxDevice.DeviceRemovedReason})"); } #endif if (exception.TargetSite != null) @@ -219,7 +233,7 @@ namespace Barotrauma { sb.AppendLine("Stack trace: "); sb.AppendLine(exception.StackTrace.CleanupStackTrace()); - sb.AppendLine("\n"); + sb.AppendLine(); } if (exception.InnerException != null) @@ -260,18 +274,43 @@ namespace Barotrauma if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } - + + string msg = string.Empty; if (GameAnalyticsManager.SendUserStatistics) { - CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers.", filePath); + msg = "A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers."; } else { - CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" + - " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/", filePath); + msg = "A crash report (\"" + filePath + "\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" + + " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/"; } + if (string.IsNullOrEmpty(dxgiErrorHelpText)) + { + msg += "\n\n" + dxgiErrorHelpText; + } + CrashMessageBox(msg, filePath); } +#if WINDOWS + private static string GetDXGIErrorHelpText(GameMain game, Exception exception) + { + string text = string.Empty; + if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) + { + var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; + var descriptor = ResultDescriptor.Find(dxDevice.DeviceRemovedReason)?.ApiCode ?? "UNKNOWN"; + + text += + $"The crash was caused by the DirectX error {descriptor} ({dxDevice.DeviceRemovedReason}). " + + "This is a common DirectX error that can be related to various different issues, such as outdated drivers, RAM problems or an overclocked or otherwise overstressed GPU. " + + "There are several potential ways to fix the issue: ensuring your graphics drivers and DirectX installation are up-to-date, disabling overclocking and adjusting various GPU-specific settings. " + + $"You may also be able to find potential solutions to the problem by using the error code {descriptor} ({dxDevice.DeviceRemovedReason}) and your GPU manufacturer as search terms."; + } + return text; + } +#endif + private static IntPtr nvApi64Dll = IntPtr.Zero; private static void EnableNvOptimus() { @@ -287,11 +326,11 @@ namespace Barotrauma private static void FreeNvOptimus() { - #warning TODO: determine if we can do this safely +#warning TODO: determine if we can do this safely //NativeLibrary.Free(nvApi64Dll); } } #endif - + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 46dfef1ef..5083a49c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -104,6 +104,7 @@ namespace Barotrauma public struct CampaignSettingElements { + public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; public SettingValue StartingFunds; @@ -114,6 +115,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), StartingBalanceAmount = StartingFunds.GetValue(), @@ -159,7 +161,7 @@ namespace Barotrauma } } - protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings) + protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings, bool isSinglePlayer) { const float verticalSize = 0.14f; @@ -180,6 +182,9 @@ namespace Barotrauma Spacing = GUI.IntScale(5) }; + SettingValue tutorialEnabled = isSinglePlayer ? + CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) : + new SettingValue(() => false, b => { }); SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); @@ -214,6 +219,7 @@ namespace Barotrauma { if (o is CampaignSettings settings) { + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); radiationEnabled.SetValue(settings.RadiationEnabled); maxMissionCountInput.SetValue(settings.MaxMissionCount); startingFundsInput.SetValue(settings.StartingBalanceAmount); @@ -226,6 +232,7 @@ namespace Barotrauma return new CampaignSettingElements { + TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, StartingFunds = startingFundsInput, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 70a8bbd8c..1a19fc973 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -35,18 +35,18 @@ namespace Barotrauma }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), string.Empty) { textFilterFunction = ToolBox.RemoveInvalidFileNameChars }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), ToolBox.RandomSeed(8)); nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 5688ea044..1b8f74e3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -370,7 +370,7 @@ namespace Barotrauma GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings, true); CampaignCustomizeSettings.Buttons[0].OnClicked += (button, o) => { @@ -608,15 +608,16 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { + var saveFolder = SaveUtil.GetSaveFolder(SaveUtil.SaveType.Singleplayer); try { - ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); + ToolBox.OpenFileWithShell(saveFolder); } catch (Exception e) { new GUIMessageBox( - TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", ("[folder]", SaveUtil.SaveFolder), ("[errormessage]", e.Message))); + TextManager.Get("error"), + TextManager.GetWithVariables("showinfoldererror", ("[folder]", saveFolder), ("[errormessage]", e.Message))); } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 2b5510ab7..f8450df3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -472,7 +472,12 @@ namespace Barotrauma { TextGetter = () => { - return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}"); + 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)); + } + return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}"); } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 668226715..6eb0b00ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -39,6 +39,7 @@ namespace Barotrauma.CharacterEditor private bool ShowExtraRagdollControls => editLimbs || editJoints; + public Character SpawnedCharacter => character; private Character character; private Vector2 spawnPosition; @@ -1513,7 +1514,7 @@ namespace Barotrauma.CharacterEditor } } - private Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) + public Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) { DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink); OnPreSpawn(); @@ -1765,9 +1766,15 @@ namespace Barotrauma.CharacterEditor var modProject = new ModProject(contentPackage); var newFile = ModProject.File.FromPath(configFilePath); modProject.AddFile(newFile); - modProject.Save(contentPackage.Path); - contentPackage = ContentPackageManager.ReloadContentPackage(contentPackage); + + var reloadResult = ContentPackageManager.ReloadContentPackage(contentPackage); + if (!reloadResult.TryUnwrapSuccess(out var newPackage)) + { + throw new Exception($"Failed to reload package", + reloadResult.TryUnwrapFailure(out var exception) ? exception : null); + } + contentPackage = newPackage; DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); @@ -3181,10 +3188,7 @@ namespace Barotrauma.CharacterEditor OnClicked = (button, data) => { ResetView(); - CharacterParams.Serialize(); - RagdollParams.Serialize(); - AnimParams.ForEach(a => a.Serialize()); - Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); + PrepareCharacterCopy(); Wizard.Instance.SelectTab(Wizard.Tab.Character); return true; } @@ -3209,9 +3213,17 @@ namespace Barotrauma.CharacterEditor fileEditPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f)); } -#endregion + #endregion -#region ToggleButtons + public void PrepareCharacterCopy() + { + CharacterParams.Serialize(); + RagdollParams.Serialize(); + AnimParams.ForEach(a => a.Serialize()); + Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); + } + + #region ToggleButtons private enum Direction { Left, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 642648f1b..64a6b65c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -101,7 +101,7 @@ namespace Barotrauma.CharacterEditor { bool isSamePackage = contentPackage.GetFiles().Any(f => Path.GetFileNameWithoutExtension(f.Path.Value) == name); LocalizedString verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); - var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) + var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }, type: GUIMessageBox.Type.Warning) { UserData = "verificationprompt" }; @@ -356,7 +356,7 @@ namespace Barotrauma.CharacterEditor } if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { - new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken")); + new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken"), type: GUIMessageBox.Type.Warning); return false; } string modName = contentPackageNameElement.Text; @@ -428,17 +428,26 @@ namespace Barotrauma.CharacterEditor texturePathElement.Flash(useRectangleFlash: true); return false; } + if (Name == CharacterPrefab.HumanSpeciesName && !IsCopy) + { + // Force a copy when trying to override a human, because handling the crash would be very difficult (we require humans to have certain definitions). + if (!CharacterEditorScreen.Instance.SpawnedCharacter.IsHuman) + { + CharacterEditorScreen.Instance.SpawnCharacter(CharacterPrefab.HumanSpeciesName); + } + CharacterEditorScreen.Instance.PrepareCharacterCopy(); + } if (IsCopy) { SourceRagdoll.Texture = evaluatedTexturePath; SourceRagdoll.CanEnterSubmarine = CanEnterSubmarine; SourceRagdoll.CanWalk = CanWalk; SourceRagdoll.Serialize(); - Wizard.Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); + Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); } else { - Wizard.Instance.SelectTab(Tab.Ragdoll); + Instance.SelectTab(Tab.Ragdoll); } return true; }; @@ -470,9 +479,6 @@ namespace Barotrauma.CharacterEditor Stretch = true, RelativeSpacing = 0.02f }; - // HTML - GUIMessageBox htmlBox = null; - var loadHtmlButton = new GUIButton(new RectTransform(new Point(content.Rect.Width / 3, elementSize), content.RectTransform), GetCharacterEditorTranslation("LoadFromHTML")); // Limbs var limbsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; @@ -689,69 +695,6 @@ namespace Barotrauma.CharacterEditor return true; } }; - loadHtmlButton.OnClicked = (b, d) => - { - if (htmlBox == null) - { - htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new LocalizedString[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); - htmlBox.Header.Font = GUIStyle.LargeFont; - var element = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); - //new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); - var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("HTMLPath").Value); - LocalizedString title = GetCharacterEditorTranslation("SelectFile"); - new GUIButton(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), title) - { - OnClicked = (button, data) => - { - FileSelection.OnFileSelected = (file) => - { - htmlPathElement.Text = file; - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("HTML", "*.html, *.htm"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.html, *.htm"); - FileSelection.Open = true; - return true; - } - }; - var list = new GUIListBox(new RectTransform(new Vector2(1, 0.8f), htmlBox.Content.RectTransform)); - var htmlOutput = new GUITextBlock(new RectTransform(Vector2.One, list.Content.RectTransform), string.Empty) { CanBeFocused = false }; - htmlBox.Buttons[0].OnClicked += (_b, _d) => - { - htmlBox.Close(); - return true; - }; - htmlBox.Buttons[1].OnClicked += (_b, _d) => - { - LimbGUIElements.ForEach(l => l.RectTransform.Parent = null); - LimbGUIElements.Clear(); - JointGUIElements.ForEach(j => j.RectTransform.Parent = null); - JointGUIElements.Clear(); - LimbXElements.Clear(); - JointXElements.Clear(); - ParseRagdollFromHTML(htmlPathElement.Text, (id, limbName, limbType, rect) => - { - CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id, limbName, limbType, rect); - }, (id1, id2, anchor1, anchor2, jointName) => - { - CreateJointGUIElement(jointsList.Content.RectTransform, elementSize, id1, id2, anchor1, anchor2, jointName); - }); - htmlOutput.Text = new XDocument(new XElement("Ragdoll", new object[] - { - new XAttribute("type", Name), LimbXElements.Values, JointXElements - })).ToString(); - htmlOutput.CalculateHeightFromText(); - list.UpdateScrollBarSize(); - return true; - }; - } - else - { - GUIMessageBox.MessageBoxes.Add(htmlBox); - } - return true; - }; // Previous box.Buttons[0].OnClicked += (b, d) => { @@ -1070,7 +1013,6 @@ namespace Barotrauma.CharacterEditor // Rectangles colliderAttributes.Add(new XAttribute("height", (int)(height * 0.85f))); colliderAttributes.Add(new XAttribute("width", (int)(width * 0.85f))); - idToCodeName.TryGetValue(id, out string notes); LimbXElements.Add(id.ToString(), new XElement("limb", new XAttribute("id", id), new XAttribute("name", limbName), @@ -1107,188 +1049,6 @@ namespace Barotrauma.CharacterEditor } } - Dictionary idToCodeName = new Dictionary(); - protected void ParseRagdollFromHTML(string path, Action limbCallback = null, Action jointCallback = null) - { - // TODO: parse as xml files -> allows to load ragdolls onto the wizard. - //XDocument doc = XMLExtensions.TryLoadXml(path); - //var xElements = doc.Elements().ToArray(); - string html = string.Empty; - try - { - html = File.ReadAllText(path); - } - catch (Exception e) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("FailedToReadHTML").Replace("[path]", path), e); - return; - } - - var lines = html.Split(new string[] { "", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height")); - int id = 0; - Dictionary hierarchyToID = new Dictionary(); - Dictionary idToHierarchy = new Dictionary(); - Dictionary idToPositionCode = new Dictionary(); - Dictionary idToName = new Dictionary(); - idToCodeName.Clear(); - foreach (var line in lines) - { - var codeNames = new string(line.SkipWhile(c => c != '>').Skip(1).ToArray()).Split(','); - for (int i = 0; i < codeNames.Length; i++) - { - string codeName = codeNames[i].Trim(); - if (string.IsNullOrWhiteSpace(codeName)) { continue; } - idToCodeName.Add(id, codeName); - string limbName = new string(codeName.SkipWhile(c => c != '_').Skip(1).ToArray()); - if (string.IsNullOrWhiteSpace(limbName)) { continue; } - idToName.Add(id, limbName); - var parts = line.Split(' '); - int ParseToInt(string selector) - { - string part = parts.First(p => p.Contains(selector)); - string s = new string(part.SkipWhile(c => c != ':').Skip(1).TakeWhile(c => char.IsNumber(c)).ToArray()); - int.TryParse(s, out int v); - return v; - }; - // example: 111311cr -> 111311 - string hierarchy = new string(codeName.TakeWhile(c => char.IsNumber(c)).ToArray()); - if (hierarchyToID.ContainsKey(hierarchy)) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("MultipleItemsWithSameHierarchy").Replace("[hierarchy]", hierarchy).Replace("[name]", codeName)); - return; - } - hierarchyToID.Add(hierarchy, id); - idToHierarchy.Add(id, hierarchy); - string positionCode = new string(codeName.SkipWhile(c => char.IsNumber(c)).TakeWhile(c => c != '_').ToArray()); - idToPositionCode.Add(id, positionCode.ToLowerInvariant()); - int x = ParseToInt("left"); - int y = ParseToInt("top"); - int width = ParseToInt("width"); - int height = ParseToInt("height"); - // This is overridden when the data is loaded from the gui fields. - LimbXElements.Add(hierarchy, new XElement("limb", - new XAttribute("id", id), - new XAttribute("name", limbName), - new XAttribute("type", ParseLimbType(limbName).ToString()), - new XElement("sprite", - new XAttribute("texture", ""), - new XAttribute("sourcerect", $"{x}, {y}, {width}, {height}")) - )); - limbCallback?.Invoke(id, limbName, ParseLimbType(limbName), new Rectangle(x, y, width, height)); - id++; - } - } - for (int i = 0; i < id; i++) - { - if (idToHierarchy.TryGetValue(i, out string hierarchy)) - { - if (hierarchy != "0") - { - // NEW LOGIC: if hierarchy length == 1, parent to 0 - // Else parent to the last bone in the current hierarchy (11 is parented to 1, 212 is parented to 21 etc) - string parent = hierarchy.Length > 1 ? hierarchy.Remove(hierarchy.Length - 1, 1) : "0"; - if (hierarchyToID.TryGetValue(parent, out int parentID)) - { - Vector2 anchor1 = Vector2.Zero; - Vector2 anchor2 = Vector2.Zero; - idToName.TryGetValue(parentID, out string parentName); - idToName.TryGetValue(i, out string limbName); - string jointName = $"{GetCharacterEditorTranslation("Joint")} {parentName} - {limbName}"; - if (idToPositionCode.TryGetValue(i, out string positionCode)) - { - float scalar = 0.8f; - if (LimbXElements.TryGetValue(parent, out XElement parentElement)) - { - Rectangle parentSourceRect = parentElement.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float parentWidth = parentSourceRect.Width / 2 * scalar; - float parentHeight = parentSourceRect.Height / 2 * scalar; - switch (positionCode) - { - case "tl": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "tc": // 0, 1 - anchor1 = new Vector2(0, parentHeight); - break; - case "tr": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "cl": // -1, 0 - anchor1 = new Vector2(-parentWidth, 0); - break; - case "cr": // 1, 0 - anchor1 = new Vector2(parentWidth, 0); - break; - case "bl": // -1, -1 - anchor1 = new Vector2(-parentWidth, -parentHeight); - break; - case "bc": // 0, -1 - anchor1 = new Vector2(0, -parentHeight); - break; - case "br": // 1, -1 - anchor1 = new Vector2(parentWidth, -parentHeight); - break; - } - if (LimbXElements.TryGetValue(hierarchy, out XElement element)) - { - Rectangle sourceRect = element.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float width = sourceRect.Width / 2 * scalar; - float height = sourceRect.Height / 2 * scalar; - switch (positionCode) - { - // Inverse - case "tl": - // br - anchor2 = new Vector2(-width, -height); - break; - case "tc": - // bc - anchor2 = new Vector2(0, -height); - break; - case "tr": - // bl - anchor2 = new Vector2(-width, -height); - break; - case "cl": - // cr - anchor2 = new Vector2(width, 0); - break; - case "cr": - // cl - anchor2 = new Vector2(-width, 0); - break; - case "bl": - // tr - anchor2 = new Vector2(-width, height); - break; - case "bc": - // tc - anchor2 = new Vector2(0, height); - break; - case "br": - // tl - anchor2 = new Vector2(-width, height); - break; - } - } - } - } - // This is overridden when the data is loaded from the gui fields. - JointXElements.Add(new XElement("joint", - new XAttribute("name", jointName), - new XAttribute("limb1", parentID), - new XAttribute("limb2", i), - new XAttribute("limb1anchor", $"{anchor1.X.Format(2)}, {anchor1.Y.Format(2)}"), - new XAttribute("limb2anchor", $"{anchor2.X.Format(2)}, {anchor2.Y.Format(2)}") - )); - jointCallback?.Invoke(parentID, i, anchor1, anchor2, jointName); - } - } - } - } - } - protected LimbType ParseLimbType(string limbName) { var limbType = LimbType.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 38202f624..66ed3c9fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -16,8 +16,8 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; Hull.EditFire = false; Hull.EditWater = false; -#endif HumanAIController.DisableCrewAI = false; +#endif } protected virtual void DeselectEditorSpecific() { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 54d5b2d8e..aa378b7af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -10,8 +10,6 @@ namespace Barotrauma { partial class GameScreen : Screen { - public override bool IsEditor => GameMain.GameSession?.GameMode is TestGameMode; - private RenderTarget2D renderTargetBackground; private RenderTarget2D renderTarget; private RenderTarget2D renderTargetWater; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index b3fb9c5ec..550a9255f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -47,7 +47,14 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; private GUIDropDown serverExecutableDropdown; - private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; + private readonly GUIButton joinServerButton, hostServerButton; + + private readonly GUIFrame modsButtonContainer; + private readonly GUIButton modsButton, modUpdatesButton; + private Task> modUpdateTask; + private float modUpdateTimer = 0.0f; + private const float ModUpdateInterval = 60.0f; + private readonly GameMain game; private GUIImage playstyleBanner; @@ -268,15 +275,29 @@ namespace Barotrauma RelativeSpacing = 0.035f }; -#if USE_STEAM - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + modsButtonContainer = new GUIFrame(new RectTransform(Vector2.One, customizeList.RectTransform), + style: null); + + modsButton = new GUIButton(new RectTransform(Vector2.One, modsButtonContainer.RectTransform), + TextManager.Get("settingstab.mods"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = ForceUpperCase.Yes, Enabled = true, UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; -#endif + + modUpdatesButton = new GUIButton(new RectTransform(Vector2.One * 0.95f, modsButtonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIUpdateButton") + { + ToolTip = TextManager.Get("ModUpdatesAvailable"), + OnClicked = (_, _) => + { + BulkDownloader.PrepareUpdates(); + return false; + }, + Visible = false + }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { @@ -334,7 +355,7 @@ namespace Barotrauma OnClicked = (button, userData) => { string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value; - GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); + GameMain.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); return true; } }; @@ -485,13 +506,17 @@ namespace Barotrauma } }; var tutorialPreview = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), tutorialContent.RectTransform)) { RelativeSpacing = 0.05f, Stretch = true }; - var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), tutorialPreview.RectTransform), style: "InnerFrame"); + var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "InnerFrame"); tutorialBanner = new GUIImage(new RectTransform(Vector2.One, imageContainer.RectTransform), style: null, scaleToFit: true); - var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); - var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); + var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) + { + AbsoluteSpacing = GUI.IntScale(10) + }; - tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.75f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont); + tutorialDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), string.Empty, wrap: true); var startButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.0f), infoContent.RectTransform, Anchor.BottomRight), text: TextManager.Get("startgamebutton")) { @@ -522,6 +547,10 @@ namespace Barotrauma private void SelectTutorial(Tutorial tutorial) { tutorialHeader.Text = tutorial.DisplayName; + tutorialHeader.CalculateHeightFromText(); + tutorialDescription.Text = tutorial.Description; + tutorialDescription.CalculateHeightFromText(); + (tutorialDescription.Parent as GUILayoutGroup)?.Recalculate(); tutorial.TutorialPrefab.Banner?.EnsureLazyLoaded(); tutorialBanner.Sprite = tutorial.TutorialPrefab.Banner; tutorialBanner.Color = tutorial.TutorialPrefab.Banner == null ? Color.Black : Color.White; @@ -541,6 +570,8 @@ namespace Barotrauma { GameMain.LuaCs.Stop(); + ResetModUpdateButton(); + if (WorkshopItemsToUpdate.Any()) { while (WorkshopItemsToUpdate.TryDequeue(out ulong workshopId)) @@ -727,6 +758,13 @@ namespace Barotrauma } #endregion + public void ResetModUpdateButton() + { + modUpdateTask = null; + modUpdateTimer = 0; + modUpdatesButton.Visible = false; + } + public void QuickStart(bool fixedSeed = false, Identifier sub = default, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) @@ -946,15 +984,36 @@ namespace Barotrauma public override void Update(double deltaTime) { -#if !DEBUG && USE_STEAM + modUpdateTimer -= (float)deltaTime; + if (modUpdateTimer <= 0.0f && modUpdateTask is not { IsCompleted: false }) + { + modUpdateTask = BulkDownloader.GetItemsThatNeedUpdating(); + modUpdateTimer = ModUpdateInterval; + } + +#if DEBUG + hostServerButton.Enabled = true; +#else if (GameSettings.CurrentConfig.UseSteamMatchmaking) { - hostServerButton.Enabled = Steam.SteamManager.IsInitialized; + hostServerButton.Enabled = SteamManager.IsInitialized; } - steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; -#elif USE_STEAM - steamWorkshopButton.Enabled = true; #endif + + if (modUpdateTask is { IsCompletedSuccessfully: true }) + { + modUpdatesButton.Visible = modUpdateTask.Result.Count > 0; + } + + if (modUpdatesButton.Visible) + { + var modButtonLabelSize = + modsButton.Font.MeasureString(modsButton.Text).ToPoint() + + new Point(GUI.IntScale(25)); + modUpdatesButton.RectTransform.AbsoluteOffset = + (modButtonLabelSize.X, modsButton.Rect.Height / 2 - modUpdatesButton.Rect.Height / 2); + } + switch (selectedTab) { case Tab.NewGame: @@ -1035,7 +1094,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) { - GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); + GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } } textPos.Y -= textSize.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index ff723980e..536ebbd7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Barotrauma.Extensions; using Barotrauma.IO; @@ -40,6 +41,13 @@ namespace Barotrauma } } + [DoesNotReturn] + private static void LogAndThrowException(string errorMsg, string analyticsId) + { + GameAnalyticsManager.AddErrorEventOnce(analyticsId, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new InvalidOperationException(errorMsg); + } + public override void Select() { base.Select(); @@ -74,20 +82,11 @@ namespace Barotrauma } }; - if (!GameMain.Client.IsServerOwner) + if (!GameMain.Client.IsServerOwner && GameMain.Client.ClientPeer.ServerContentPackages.Length == 0) { - if (GameMain.Client.ClientPeer.ServerContentPackages.Length == 0) - { - string errorMsg = $"Error in ModDownloadScreen: the list of mods the server has enabled was empty. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}"; - GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new InvalidOperationException(errorMsg); - } - if (GameMain.Client.ClientPeer.ServerContentPackages.None(p => p.CorePackage != null)) - { - string errorMsg = $"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}"; - GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoCorePackage", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new InvalidOperationException(errorMsg); - } + LogAndThrowException("Error in ModDownloadScreen: the list of mods the server has enabled was empty. " + +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}", + analyticsId: "ModDownloadScreen.Select:NoContentPackages"); } var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages @@ -96,11 +95,18 @@ namespace Barotrauma { if (!GameMain.Client.IsServerOwner) { + var corePackage = GameMain.Client.ClientPeer.ServerContentPackages + .Select(p => p.CorePackage) + .OfType().FirstOrDefault(); + if (corePackage is null) + { + LogAndThrowException($"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. " + + $"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}", + analyticsId: "ModDownloadScreen.Select:NoCorePackage"); + } + ContentPackageManager.EnabledPackages.BackUp(); - ContentPackageManager.EnabledPackages.SetCore( - GameMain.Client.ClientPeer.ServerContentPackages - .Select(p => p.CorePackage) - .OfType().First()); + ContentPackageManager.EnabledPackages.SetCore(corePackage); List regularPackages = GameMain.Client.ClientPeer.ServerContentPackages .Select(p => p.RegularPackage) @@ -114,6 +120,15 @@ namespace Barotrauma return; } + if (missingPackages.FirstOrDefault(p => p.IsVanilla) is { } mismatchedVanilla) + { + LogAndThrowException("Error in ModDownloadScreen: mismatched Vanilla package: " + +$"local hash is {ContentPackageManager.VanillaCorePackage?.Hash.StringRepresentation ?? "[NULL]"}, " + +$"remote hash is {mismatchedVanilla.Hash.StringRepresentation}. " + +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}", + analyticsId: "ModDownloadScreen.Select:MismatchedVanilla"); + } + GUIMessageBox msgBox = new GUIMessageBox( TextManager.Get("ModDownloadTitle"), "", @@ -292,14 +307,23 @@ namespace Barotrauma var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages; CorePackage corePackage = downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage - ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) - ?.CorePackage + ?? serverPackages.FirstOrDefault(p => p.CorePackage != null)?.CorePackage ?? throw new Exception($"Failed to find core package to enable"); List regularPackages = new List(); foreach (var p in serverPackages) { - if (p.CorePackage != null) { continue; } + if (p.CorePackage != null) + { + // This package is one of our installed core packages + continue; + } + + if (corePackage.Hash.Equals(p.Hash)) + { + // This package is the core package we downloaded from the server + continue; + } RegularPackage? matchingPackage = p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage; if (matchingPackage is null) @@ -357,9 +381,13 @@ namespace Barotrauma string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); SaveUtil.DecompressToDirectory(path, dir, file => { }); - ContentPackage newPackage - = ContentPackage.TryLoad($"{dir}/{ContentPackage.FileListFileName}") - ?? throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\""); + var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName)); + + if (!result.TryUnwrapSuccess(out var newPackage)) + { + throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\"", + result.TryUnwrapFailure(out var exception) ? exception : null); + } if (!currentDownload.Hash.Equals(newPackage.Hash)) { throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 6d966c50e..c6956035d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -103,6 +103,10 @@ namespace Barotrauma public GUIFrame JobPreferenceContainer; public GUIListBox JobList; + private Identifier micIconStyle; + private float micCheckTimer; + const float MicCheckInterval = 1.0f; + private float autoRestartTimer; //persistent characterinfo provided by the server @@ -2656,27 +2660,9 @@ namespace Barotrauma public override void Update(double deltaTime) { - base.Update(deltaTime); - if (GameMain.Client == null) { return; } - Identifier currMicStyle = micIcon.Style.Element.NameAsIdentifier(); - - Identifier targetMicStyle = "GUIMicrophoneEnabled".ToIdentifier(); - var voipCaptureDeviceNames = VoipCapture.CaptureDeviceNames; - if (voipCaptureDeviceNames.Count == 0) - { - targetMicStyle = "GUIMicrophoneUnavailable".ToIdentifier(); - } - else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) - { - targetMicStyle = "GUIMicrophoneDisabled".ToIdentifier(); - } - - if (targetMicStyle != currMicStyle) - { - GUIStyle.Apply(micIcon, targetMicStyle); - } + UpdateMicIcon((float)deltaTime); foreach (GUIComponent child in PlayerList.Content.Children) { @@ -2738,6 +2724,35 @@ namespace Barotrauma if (!mouseRect.Contains(PlayerInput.MousePosition)) { jobVariantTooltip = null; } } } + + private void UpdateMicIcon(float deltaTime) + { + micCheckTimer -= deltaTime; + if (micCheckTimer > 0.0f) { return; } + + Identifier newMicIconStyle = "GUIMicrophoneEnabled".ToIdentifier(); + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) + { + newMicIconStyle = "GUIMicrophoneDisabled".ToIdentifier(); + } + else + { + var voipCaptureDeviceNames = VoipCapture.GetCaptureDeviceNames(); + if (voipCaptureDeviceNames.Count == 0) + { + newMicIconStyle = "GUIMicrophoneUnavailable".ToIdentifier(); + } + } + + if (newMicIconStyle != micIconStyle) + { + micIconStyle = newMicIconStyle; + GUIStyle.Apply(micIcon, newMicIconStyle); + } + + micCheckTimer = MicCheckInterval; + } + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { graphics.Clear(Color.Black); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index dcb6ab842..f86553cb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1352,6 +1352,10 @@ namespace Barotrauma private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) { + if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; } + if (serverInfo.PlayerCount < 0) { return; } + if (serverInfo.MaxPlayers <= 0) { return; } + RemoveMsgFromServerList(MsgUserData.RefreshingServerList); RemoveMsgFromServerList(MsgUserData.NoServers); var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index df8fc28ba..f32050a63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -379,6 +379,9 @@ namespace Barotrauma void CreateSprite(ContentXElement element) { + //empty element, probably an item variant? + if (element.Attributes().None()) { return; } + string spriteFolder = ""; ContentPath texturePath = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index ab74c02ec..c4eb4b4c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -208,7 +208,7 @@ namespace Barotrauma private GUIFrame wiringToolPanel; - private DateTime editorSelectedTime; + private Option editorSelectedTime; private GUIImage previewImage; private GUILayoutGroup previewImageButtonHolder; @@ -354,9 +354,24 @@ namespace Barotrauma ToolTip = RichString.Rich(TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖"), OnClicked = (btn, data) => { +#if DEBUG + if (ContentPackageManager.EnabledPackages.All.Any(cp => cp != ContentPackageManager.VanillaCorePackage && cp.Files.Any(f => f is not BaseSubFile))) + { + var msgBox = new GUIMessageBox("DEBUG-ONLY WARNING", "You currently have some mods enabled. Are you sure you want to save the submarine? If the mods override any vanilla content, saving the submarine may cause unintended changes.", + new LocalizedString[] { "Yes, I know what I'm doing", "Cancel" }); + msgBox.Buttons[0].OnClicked = (btn, data) => + { + msgBox.Close(); + loadFrame = null; + CreateSaveScreen(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + return false; + } +#endif loadFrame = null; CreateSaveScreen(); - return true; } }; @@ -984,6 +999,7 @@ namespace Barotrauma foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory))) { + if (category == MapEntityCategory.None) { continue; } entityCategoryButtons.Add(new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), entityMenuTop.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CategoryButton." + category.ToString()) { @@ -1071,6 +1087,7 @@ namespace Barotrauma foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory))) { + if (category == MapEntityCategory.None) { continue; } LocalizedString categoryName = TextManager.Get("MapEntityCategory." + category); maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(categoryName.Replace(" ", "\n")).X + GUI.IntScale(50)); foreach (MapEntityPrefab ep in MapEntityPrefab.List) @@ -1394,7 +1411,7 @@ namespace Barotrauma if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); - editorSelectedTime = DateTime.Now; + editorSelectedTime = Option.Some(DateTime.Now); GUI.ForceMouseOn(null); SetMode(Mode.Default); @@ -1543,9 +1560,13 @@ namespace Barotrauma autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); autoSaveLabel = null; - TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM - SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) + { + TimeSpan timeInEditor = DateTime.Now - selectedTime; + SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + editorSelectedTime = Option.None(); + } #endif GUI.ForceMouseOn(null); @@ -2490,7 +2511,7 @@ namespace Barotrauma { IntValue = MainSub.Info.Tier, MinValueInt = 1, - MaxValueInt = 3, + MaxValueInt = SubmarineInfo.HighestTier, OnValueChanged = (numberInput) => { MainSub.Info.Tier = numberInput.IntValue; @@ -2498,7 +2519,7 @@ namespace Barotrauma }; if (MainSub?.Info != null) { - MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, 3); + MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, SubmarineInfo.HighestTier); } var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -3074,11 +3095,17 @@ namespace Barotrauma XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); doc.SaveSafe(filePath); - - var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; - if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) + + var result = ContentPackageManager.ReloadContentPackage(existingContentPackage); + if (!result.TryUnwrapSuccess(out var resultPackage)) { - ContentPackageManager.EnabledPackages.EnableRegular(resultPackage); + throw new Exception($"Failed to reload content package \"{existingContentPackage.Name}\"", + result.TryUnwrapFailure(out var exception) ? exception : null); + } + if (resultPackage is RegularPackage regularPackage + && !ContentPackageManager.EnabledPackages.Regular.Contains(regularPackage)) + { + ContentPackageManager.EnabledPackages.EnableRegular(regularPackage); GameSettings.SaveCurrentConfig(); } @@ -3089,7 +3116,7 @@ namespace Barotrauma return false; } - private void SnapToGrid() + private static void SnapToGrid() { // First move components foreach (MapEntity e in MapEntity.SelectedList) @@ -3102,6 +3129,10 @@ namespace Barotrauma var wire = item.GetComponent(); if (wire != null) { continue; } item.Move(offset); + if (item.GetComponent()?.LinkedGap is Gap linkedGap) + { + linkedGap.Move(item.Position - linkedGap.Position); + } } else if (e is Structure structure) { @@ -3126,7 +3157,7 @@ namespace Barotrauma } } - private IEnumerable GetLoadableSubs() + private static IEnumerable GetLoadableSubs() { string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); return SubmarineInfo.SavedSubmarines.Where(s @@ -3231,7 +3262,7 @@ namespace Barotrauma } string pathWithoutUserName = Path.GetFullPath(sub.FilePath); - string saveFolder = Path.GetFullPath(SaveUtil.SaveFolder); + string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder); if (pathWithoutUserName.StartsWith(saveFolder)) { pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; @@ -3513,9 +3544,18 @@ namespace Barotrauma public void LoadSub(SubmarineInfo info) { Submarine.Unload(); - var selectedSub = new Submarine(info); - MainSub = selectedSub; - MainSub.UpdateTransform(interpolate: false); + Submarine selectedSub = null; + try + { + selectedSub = new Submarine(info); + MainSub = selectedSub; + MainSub.UpdateTransform(interpolate: false); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to load the submarine. The submarine file might be corrupted.", e); + return; + } ClearUndoBuffer(); CreateDummyCharacter(); @@ -4220,7 +4260,8 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { PlaySoundOnSelect = true, - OnSelected = SelectWire + OnSelected = SelectWire, + CanTakeKeyBoardFocus = false }; List wirePrefabs = new List(); @@ -5869,7 +5910,7 @@ namespace Barotrauma decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2); Vector2 offset = new Vector2(GUI.IntScale(24)); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance} m", GUIStyle.TextColorNormal, font: GUIStyle.Font, backgroundColor: Color.Black, backgroundPadding: 4); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index abfbb86e2..e36471206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -21,6 +21,7 @@ namespace Barotrauma public static Character? dummyCharacter; public static Effect? BlueprintEffect; + public TabMenu? TabMenu; public TestScreen() { @@ -49,9 +50,10 @@ namespace Barotrauma } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom(Rand.RandSync.Unsynced)); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "assistant")); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); + dummyCharacter.Info.GiveExperience(999999); miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false); @@ -61,6 +63,7 @@ namespace Barotrauma } Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); + TabMenu = new TabMenu(); } public override void AddToGUIUpdateList() @@ -68,35 +71,37 @@ namespace Barotrauma Frame.AddToGUIUpdateList(); CharacterHUD.AddToGUIUpdateList(dummyCharacter); dummyCharacter?.SelectedItem?.AddToGUIUpdateList(); + TabMenu?.AddToGUIUpdateList(); } public override void Update(double deltaTime) { base.Update(deltaTime); + TabMenu?.Update((float)deltaTime); - if (dummyCharacter is { } dummy && miniMapItem is { } item) - { - if (dummy.SelectedItem != item) - { - dummy.SelectedItem = item; - } - - dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime); - Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); - - foreach (Limb limb in dummy.AnimController.Limbs) - { - limb.body.SetTransform(pos, 0.0f); - } - - if (dummy.AnimController?.Collider is { } collider) - { - collider.SetTransform(pos, 0); - } - - dummy.ControlLocalPlayer((float)deltaTime, Cam, false); - dummy.Control((float)deltaTime, Cam); - } + // if (dummyCharacter is { } dummy && miniMapItem is { } item) + // { + // if (dummy.SelectedConstruction != item) + // { + // dummy.SelectedConstruction = item; + // } + // + // dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime); + // Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); + // + // foreach (Limb limb in dummy.AnimController.Limbs) + // { + // limb.body.SetTransform(pos, 0.0f); + // } + // + // if (dummy.AnimController?.Collider is { } collider) + // { + // collider.SetTransform(pos, 0); + // } + // + // dummy.ControlLocalPlayer((float)deltaTime, Cam, false); + // dummy.Control((float)deltaTime, Cam); + // } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index a63e7c905..c3c4c3873 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -1332,16 +1332,18 @@ namespace Barotrauma } } - private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) + private static void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) { - if (entity is ItemComponent e) + if (GameMain.Client != null) { - entity = e.Item; - } - - if (GameMain.Client != null && entity is Item item) - { - GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property)); + if (entity is Item item) + { + GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property, item)); + } + else if (entity is ItemComponent ic) + { + GameMain.Client.CreateEntityEvent(ic.Item, new Item.ChangePropertyEventData(property, ic)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 65a6e87bc..594e36ad4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -176,7 +176,7 @@ namespace Barotrauma Action setter) where T : Enum => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); - private static void Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) + private static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) { var dropdown = new GUIDropDown(NewItemRectT(parent)); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); @@ -189,9 +189,10 @@ namespace Barotrauma setter((T)obj); return true; }; + return dropdown; } - private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) + private static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") @@ -213,11 +214,12 @@ namespace Barotrauma setter(sb.BarScrollValue); return true; }; + return (slider, label); } - private void Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) + private static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) { - var tickbox = new GUITickBox(NewItemRectT(parent), label) + return new GUITickBox(NewItemRectT(parent), label) { Selected = currentValue, ToolTip = tooltip, @@ -231,7 +233,7 @@ namespace Barotrauma private string Percentage(float v) => ToolBox.GetFormattedPercentage(v); - private int Round(float v) => (int)MathF.Round(v); + private static int Round(float v) => MathUtils.RoundToInt(v); private void CreateGraphicsTab() { @@ -262,30 +264,30 @@ namespace Barotrauma Spacer(left); Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont); - DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, (v) => unsavedConfig.Graphics.DisplayMode = v); + DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, v => unsavedConfig.Graphics.DisplayMode = v); Spacer(left); - Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, (v) => unsavedConfig.Graphics.VSync = v); - Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, (v) => unsavedConfig.Graphics.CompressTextures = v); + Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); + Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); - DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, (v) => unsavedConfig.Graphics.LosMode = v); + DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v); Spacer(right); Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); - Slider(right, (0.5f, 1.0f), 11, (v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); + Slider(right, (0.5f, 1.0f), 11, v => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, v => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); Spacer(right); Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); - Slider(right, (10, 210), 21, (v) => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, - (v) => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); + Slider(right, (10, 210), 21, v => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, + v => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); Spacer(right); - Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, (v) => unsavedConfig.Graphics.RadialDistortion = v); - Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, (v) => unsavedConfig.Graphics.ChromaticAberration = v); + Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, v => unsavedConfig.Graphics.RadialDistortion = v); + Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, v => unsavedConfig.Graphics.ChromaticAberration = v); Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); - Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); + Slider(right, (100, 1500), 15, v => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, v => unsavedConfig.Graphics.ParticleLimit = Round(v)); Spacer(right); } @@ -399,23 +401,23 @@ namespace Barotrauma Spacer(audio); Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, (v) => unsavedConfig.Audio.SoundVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v => unsavedConfig.Audio.SoundVolume = v); Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v => unsavedConfig.Audio.MusicVolume = v); Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, (v) => unsavedConfig.Audio.UiVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v => unsavedConfig.Audio.UiVolume = v); - Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); - Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, v => unsavedConfig.Audio.MuteOnFocusLost = v); + Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, v => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, (v) => unsavedConfig.Audio.VoiceChatVolume = v); + Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v => unsavedConfig.Audio.VoiceChatVolume = v); - Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, (v) => unsavedConfig.Audio.UseDirectionalVoiceChat = v); - Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, (v) => unsavedConfig.Audio.VoipAttenuationEnabled = v); + Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, v => unsavedConfig.Audio.UseDirectionalVoiceChat = v); + Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, v => unsavedConfig.Audio.VoipAttenuationEnabled = v); Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont); @@ -424,7 +426,7 @@ namespace Barotrauma Spacer(voiceChat); Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); - DropdownEnum(voiceChat, (v) => TextManager.Get($"VoiceMode.{v}"), (v) => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, (v) => unsavedConfig.Audio.VoiceSetting = v); + DropdownEnum(voiceChat, v => TextManager.Get($"VoiceMode.{v}"), v => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, v => unsavedConfig.Audio.VoiceSetting = v); Spacer(voiceChat); var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont); @@ -464,11 +466,11 @@ namespace Barotrauma Spacer(voiceChat); Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont); - Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, (v) => unsavedConfig.Audio.MicrophoneVolume = v); + Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, v => unsavedConfig.Audio.MicrophoneVolume = v); Spacer(voiceChat); Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont); - Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); + Slider(voiceChat, (0, 500), 26, v => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, v => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); } private readonly Dictionary> inputButtonValueNameGetters = new Dictionary>(); @@ -481,8 +483,10 @@ namespace Barotrauma GUILayoutGroup layout = CreateCenterLayout(content); Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont); - Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, (v) => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); - Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, (v) => unsavedConfig.EnableMouseLook = v); + + var aimAssistSlider = Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, v => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); + Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, v => unsavedConfig.EnableMouseLook = v); + Spacer(layout); GUIListBox keyMapList = @@ -523,7 +527,7 @@ namespace Barotrauma if (willBeSelected) { inputBoxSelectedThisFrame = true; - currentSetter = (v) => + currentSetter = v => { valueSetter(v); btn.Text = valueNameGetter(); @@ -626,7 +630,7 @@ namespace Barotrauma currRow, TextManager.Get($"InputType.{input}"), () => unsavedConfig.KeyMap.Bindings[input].Name, - (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), + v => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), LegacyInputTypes.Contains(input)); } } @@ -644,7 +648,7 @@ namespace Barotrauma currRow, TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)), () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, - (v) => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); + v => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); } } @@ -663,6 +667,8 @@ namespace Barotrauma { unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); + aimAssistSlider.slider.BarScrollValue = GameSettings.Config.DefaultAimAssist; + aimAssistSlider.label.Text = Percentage(GameSettings.Config.DefaultAimAssist); foreach (var btn in inputButtonValueNameGetters.Keys) { btn.Text = inputButtonValueNameGetters[btn](); @@ -683,13 +689,13 @@ namespace Barotrauma .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); Label(layout, TextManager.Get("Language"), GUIStyle.SubHeadingFont); - Dropdown(layout, (v) => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, (v) => unsavedConfig.Language = v); + Dropdown(layout, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); Spacer(layout); - Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, (v) => unsavedConfig.PauseOnFocusLost = v); + Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); Spacer(layout); - Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, (v) => unsavedConfig.DisableInGameHints = v); + Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") @@ -710,13 +716,17 @@ namespace Barotrauma } }; Spacer(layout); - + + Label(layout, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); + DropdownEnum(layout, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); + Spacer(layout); + Label(layout, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, (v) => unsavedConfig.Graphics.HUDScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); Label(layout, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, (v) => unsavedConfig.Graphics.InventoryScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, (v) => unsavedConfig.Graphics.TextScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); #if !OSX Spacer(layout); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 20f85f5e4..1f1dca9e1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Sounds private VorbisReader reader; //key = sample rate, value = filter - private static Dictionary muffleFilters = new Dictionary(); + private static readonly Dictionary muffleFilters = new Dictionary(); private static List playbackAmplitude; private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file @@ -29,8 +29,8 @@ namespace Barotrauma.Sounds public override int FillStreamBuffer(int samplePos, short[] buffer) { - if (!Stream) throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); - if (reader == null) throw new Exception("Called FillStreamBuffer when the reader is null!"); + if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); } + if (reader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 3f4918c57..d911d4ac2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -24,11 +24,12 @@ namespace Barotrauma.Sounds public readonly bool StreamsReliably; + private readonly SoundManager.SourcePoolIndex sourcePoolIndex = SoundManager.SourcePoolIndex.Default; public virtual SoundManager.SourcePoolIndex SourcePoolIndex { get { - return SoundManager.SourcePoolIndex.Default; + return sourcePoolIndex; } } @@ -59,13 +60,14 @@ namespace Barotrauma.Sounds public float BaseNear; public float BaseFar; - public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement=null, bool getFullPath=true) + public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement = null, bool getFullPath = true) { Owner = owner; Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename; Stream = stream; StreamsReliably = streamsReliably; XElement = xElement; + sourcePoolIndex = XElement.GetAttributeEnum("sourcepool", SoundManager.SourcePoolIndex.Default); BaseGain = 1.0f; BaseNear = 100.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index bacb39916..6f5b2efac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -1,9 +1,7 @@ -using System; +using Microsoft.Xna.Framework; using OpenAL; -using Microsoft.Xna.Framework; -using System.Collections.Generic; +using System; using System.Threading; -using System.Diagnostics; namespace Barotrauma.Sounds { @@ -17,7 +15,7 @@ namespace Barotrauma.Sounds public SoundSourcePool(int sourceCount = SoundManager.SOURCE_COUNT) { - int alError = Al.NoError; + int alError; ALSources = new uint[sourceCount]; for (int i = 0; i < sourceCount; i++) @@ -83,7 +81,7 @@ namespace Barotrauma.Sounds class SoundChannel : IDisposable { private const int STREAM_BUFFER_SIZE = 8820; - private short[] streamShortBuffer; + private readonly short[] streamShortBuffer; private string debugName = "SoundChannel"; @@ -312,12 +310,12 @@ namespace Barotrauma.Sounds if (ALSourceIndex < 0) { return; } - if (!IsPlaying) return; + if (!IsPlaying) { return; } if (!IsStream) { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); - int playbackPos; Al.GetSourcei(alSource, Al.SampleOffset, out playbackPos); + Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); int alError = Al.GetError(); if (alError != Al.NoError) { @@ -379,7 +377,7 @@ namespace Barotrauma.Sounds if (!IsStream) { - int playbackPos; Al.GetSourcei(alSource, Al.SampleOffset, out playbackPos); + Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); int alError = Al.GetError(); if (alError != Al.NoError) { @@ -390,7 +388,7 @@ namespace Barotrauma.Sounds } else { - float retVal = -1.0f; + float retVal; Monitor.Enter(mutex); retVal = streamAmplitude; Monitor.Exit(mutex); @@ -432,8 +430,8 @@ namespace Barotrauma.Sounds private bool reachedEndSample; private int queueStartIndex; private readonly uint[] streamBuffers; - private uint[] unqueuedBuffers; - private float[] streamBufferAmplitudes; + private readonly uint[] unqueuedBuffers; + private readonly float[] streamBufferAmplitudes; public int StreamSeekPos { @@ -448,18 +446,17 @@ namespace Barotrauma.Sounds } } - private object mutex; + private readonly object mutex; public bool IsPlaying { get { - if (ALSourceIndex < 0) return false; - if (IsStream && !reachedEndSample) return true; - int state; + if (ALSourceIndex < 0) { return false; } + if (IsStream && !reachedEndSample) { return true; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); - if (!Al.IsSource(alSource)) return false; - Al.GetSourcei(alSource, Al.SourceState, out state); + if (!Al.IsSource(alSource)) { return false; } + Al.GetSourcei(alSource, Al.SourceState, out int state); int alError = Al.GetError(); if (alError != Al.NoError) { @@ -710,8 +707,7 @@ namespace Barotrauma.Sounds { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); - int state; - Al.GetSourcei(alSource, Al.SourceState, out state); + Al.GetSourcei(alSource, Al.SourceState, out int state); bool playing = state == Al.Playing; int alError = Al.GetError(); if (alError != Al.NoError) @@ -719,8 +715,7 @@ namespace Barotrauma.Sounds throw new Exception("Failed to determine playing state from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } - int unqueuedBufferCount; - Al.GetSourcei(alSource, Al.BuffersProcessed, out unqueuedBufferCount); + Al.GetSourcei(alSource, Al.BuffersProcessed, out int unqueuedBufferCount); alError = Al.GetError(); if (alError != Al.NoError) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 5870847e0..bb5f74966 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -716,8 +716,7 @@ namespace Barotrauma if (Screen.Selected == null) { return "menu".ToIdentifier(); } - if ((Screen.Selected?.IsEditor ?? false) - || (Screen.Selected == GameMain.NetLobbyScreen)) + if (Screen.Selected is { IsEditor: true } || GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected == GameMain.NetLobbyScreen) { return "editor".ToIdentifier(); } @@ -814,7 +813,7 @@ namespace Barotrauma { return "levelend".ToIdentifier(); } - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 && + if (GameMain.GameSession.RoundDuration < 120.0 && Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { return "start".ToIdentifier(); @@ -864,22 +863,21 @@ namespace Barotrauma PlayDamageSound(damageType, damage, bodyPosition, 800.0f); } - private static readonly List tempList = new List(); public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { - damage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); - tempList.Clear(); - foreach (var s in damageSounds) - { - if ((s.DamageRange == Vector2.Zero || - (damage >= s.DamageRange.X && damage <= s.DamageRange.Y)) && - s.DamageType == damageType && - (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))) - { - tempList.Add(s); - } - } - var damageSound = tempList.GetRandomUnsynced(); + //if the damage is too low for any sound, don't play anything + if (damageSounds.All(d => damage < d.DamageRange.X)) { return; } + + //allow the damage to differ by 10 from the configured damage range, + //so the same amount of damage doesn't always play the same sound + float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); + + var suitableSounds = damageSounds.Where(s => + s.DamageType == damageType && + (s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)) && + (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))); + + var damageSound = suitableSounds.GetRandomUnsynced(); damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 7c52cd028..8e84334b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -1,10 +1,8 @@ -using Barotrauma.IO; -using Barotrauma.Networking; +using Barotrauma.Networking; using Concentus.Structs; using Microsoft.Xna.Framework; using OpenAL; using System; -using System.Collections.Generic; namespace Barotrauma.Sounds { @@ -26,12 +24,12 @@ namespace Barotrauma.Sounds } } - private VoipQueue queue; + private readonly VoipQueue queue; private int bufferID = 0; private SoundChannel soundChannel; - private OpusDecoder decoder; + private readonly OpusDecoder decoder; public bool UseRadioFilter; public bool UseMuffleFilter; @@ -39,11 +37,11 @@ namespace Barotrauma.Sounds public float Near { get; private set; } public float Far { get; private set; } - private BiQuad[] muffleFilters = new BiQuad[] + private readonly BiQuad[] muffleFilters = new BiQuad[] { new LowpassFilter(VoipConfig.FREQUENCY, 800) }; - private BiQuad[] radioFilters = new BiQuad[] + private readonly BiQuad[] radioFilters = new BiQuad[] { new BandpassFilter(VoipConfig.FREQUENCY, 2000) }; @@ -101,13 +99,14 @@ namespace Barotrauma.Sounds public void ApplyFilters(short[] buffer, int readSamples) { + float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume; for (int i = 0; i < readSamples; i++) { float fVal = ShortToFloat(buffer[i]); - if (gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume > 1.0f) //TODO: take distance into account? + if (finalGain > 1.0f) //TODO: take distance into account? { - fVal = Math.Clamp(fVal * gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume, -1f, 1f); + fVal = Math.Clamp(fVal * finalGain, -1f, 1f); } if (UseMuffleFilter) diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 1f59cf11e..9927c01b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -21,6 +21,13 @@ namespace Barotrauma private Entity soundEmitter; private double loopStartTime; private bool loopSound; + /// + /// Each new sound overrides the existing sounds that were launched with this status effect, meaning the old sound will be faded out and disposed and the new sound will be played instead of the old. + /// Normally the call to play the sound is ignored if there's an existing sound playing when the effect triggers. + /// Used for example for ensuring that rapid playing sounds restart playing even when the previous clip(s) have not yet stopped. + /// Use with caution. + /// + private bool forcePlaySounds; partial void InitProjSpecific(ContentXElement element, string parentDebugName) { @@ -50,6 +57,7 @@ namespace Barotrauma break; } } + forcePlaySounds = element.GetAttributeBool(nameof(forcePlaySounds), false); } partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull hull, Vector2 worldPosition, bool playSound) @@ -71,7 +79,7 @@ namespace Barotrauma { angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); particleRotation = -item.body.Rotation; - if (item.body.Dir < 0.0f) + if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; mirrorAngle = true; @@ -98,7 +106,7 @@ namespace Barotrauma particleRotation = -targetLimb.body.Rotation; float offset = targetLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; particleRotation += offset; - if (targetLimb.body.Dir < 0.0f) + if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; mirrorAngle = true; @@ -114,10 +122,14 @@ namespace Barotrauma private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition) { - if (sounds.Count == 0) return; + if (sounds.Count == 0) { return; } - if (soundChannel == null || !soundChannel.IsPlaying) + if (soundChannel == null || !soundChannel.IsPlaying || forcePlaySounds) { + if (soundChannel != null && soundChannel.IsPlaying) + { + soundChannel.FadeOutAndDispose(); + } if (soundSelectionMode == SoundSelectionMode.All) { foreach (RoundSound sound in sounds) @@ -128,7 +140,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling); + soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); ignoreMuffling = sound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } } @@ -155,7 +167,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); ignoreMuffling = selectedSound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index da5f50871..27c008170 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -64,7 +64,7 @@ namespace Barotrauma.Steam }); } - private static async Task> GetItemsThatNeedUpdating() + public static async Task> GetItemsThatNeedUpdating() { var determiningTasks = ContentPackageManager.WorkshopPackages.Select(async p => (p, await p.IsUpToDate())); (ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks); @@ -126,6 +126,7 @@ namespace Barotrauma.Steam { mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); } + GameMain.MainMenuScreen.ResetModUpdateButton(); }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 54cab99e7..258594f3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -50,20 +50,25 @@ namespace Barotrauma.Steam lobbyState = LobbyState.Owner; lobbyID = (currentLobby?.Id).Value; - if (serverSettings.IsPublic) - { - currentLobby?.SetPublic(); - } - else - { - currentLobby?.SetFriendsOnly(); - } + SetLobbyPublic(serverSettings.IsPublic); currentLobby?.SetJoinable(true); UpdateLobby(serverSettings); }); } + public static void SetLobbyPublic(bool isPublic) + { + if (isPublic) + { + currentLobby?.SetPublic(); + } + else + { + currentLobby?.SetFriendsOnly(); + } + } + public static void UpdateLobby(ServerSettings serverSettings) { if (GameMain.Client == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 66463fe83..26b32f26c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -73,7 +73,13 @@ namespace Barotrauma.Steam //This callback seems to take place when the item has been downloaded recently and an update //or a redownload has taken place - Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => Workshop.OnItemDownloadComplete(id); + Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => + { + if (result == Steamworks.Result.OK) + { + Workshop.OnItemDownloadComplete(id); + } + }; //Maybe I'm completely wrong! All I know is that we need to handle both! } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 370b47f3f..124e77d1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -177,7 +177,13 @@ namespace Barotrauma.Steam await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); - ContentPackage tempPkg = ContentPackage.TryLoad(stagingFileListPath) ?? throw new Exception("Staging copy could not be loaded"); + + var result = ContentPackage.TryLoad(stagingFileListPath); + if (!result.TryUnwrapSuccess(out var tempPkg)) + { + throw new Exception("Staging copy could not be loaded", + result.TryUnwrapFailure(out var exception) ? exception : null); + } //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be ModProject modProject = new ModProject(tempPkg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index 7cc29acce..8c43da578 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Steam CanBeFocused = false, UserData = p }; - if (p.Errors.Any()) + if (p.FatalLoadErrors.Any()) { CreateModErrorInfo(p, regularBox, regularBox); regularBox.CanBeFocused = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index cce1627d8..4331f34bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -49,21 +49,10 @@ namespace Barotrauma.Steam SteamManager.Workshop.DownloadModThenEnqueueInstall(item); } } - - TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => + + SteamManager.Workshop.DeleteUnsubscribedMods(removedPackages => { - if (!t.TryGetResult(out ISet publishedItems)) { return; } - - var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); - bool needsRefresh = false; - foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - SteamManager.Workshop.Uninstall(item); - needsRefresh = true; - } - - if (needsRefresh) + if (removedPackages.Any()) { PopulateInstalledModLists(); } @@ -93,7 +82,7 @@ namespace Barotrauma.Steam return (left, center, right); } - private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) + private static void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) { if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) { @@ -197,7 +186,11 @@ namespace Barotrauma.Steam out onInstalledInfoButtonHit, out var deselect); GUILayoutGroup mainLayout = - new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(5) + }; mainLayout.RectTransform.SetAsFirstChild(); var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); @@ -257,7 +250,12 @@ namespace Barotrauma.Steam right.ChildAnchor = Anchor.TopRight; //enabled mods - Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var label = Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + new GUIImage(new RectTransform(new Point(label.Rect.Height), label.RectTransform, Anchor.CenterRight), style: "GUIButtonInfo") + { + ToolTip = TextManager.Get("ModLoadOrderExplanation") + }; + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) { CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, @@ -478,8 +476,9 @@ namespace Barotrauma.Steam { string str = modsListFilter.Text; enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) - .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) - || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); + .ForEach(c + => c.Visible = c.UserData is not ContentPackage p + || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); } private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem) @@ -504,12 +503,12 @@ namespace Barotrauma.Steam //are enabled, and all files match either of them so show this mod } else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected - && p.Files.Any(f => !(f is BaseSubFile))) + && p.Files.Any(f => f is not BaseSubFile)) { matches = false; } else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected - && p.Files.Any(f => !(f is ItemAssemblyFile))) + && p.Files.Any(f => f is not ItemAssemblyFile)) { matches = false; } @@ -520,7 +519,7 @@ namespace Barotrauma.Steam private void PrepareToShowModInfo(ContentPackage mod) { if (!mod.UgcId.TryUnwrap(out var ugcId) - || !(ugcId is SteamWorkshopId workshopId)) { return; } + || ugcId is not SteamWorkshopId workshopId) { return; } TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value), t => { @@ -541,7 +540,20 @@ namespace Barotrauma.Steam (p) => p.Name, ContentPackageManager.CorePackages.ToArray(), ContentPackageManager.EnabledPackages.Core!, - (p) => { }); + (p) => + { + enabledCoreDropdown.ButtonTextColor = + p.HasAnyErrors + ? GUIStyle.Red + : GUIStyle.TextColorNormal; + }); + enabledCoreDropdown.ListBox.Content.Children + .OfType() + .ForEach(tb => + CreateModErrorInfo( + (tb.UserData as ContentPackage)!, + tb, + tb)); void addRegularModToList(RegularPackage mod, GUIListBox list) { @@ -649,10 +661,7 @@ namespace Barotrauma.Steam { CanBeFocused = false }; - if (mod.Errors.Any()) - { - CreateModErrorInfo(mod, modFrame, modName); - } + CreateModErrorInfo(mod, modFrame, modName); if (ContentPackageManager.LocalPackages.Contains(mod)) { var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index c9cbd489d..b69efd9b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -44,12 +44,26 @@ namespace Barotrauma.Steam public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout - = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); + = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(4) + }; tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; tabContents = new Dictionary(); + new GUIButton(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform, Anchor.BottomLeft), + style: "GUIButtonSmall", text: TextManager.Get("FindModsButton")) + { + OnClicked = (button, o) => + { + SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); + return false; + } + }; + contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform), @@ -130,17 +144,8 @@ namespace Barotrauma.Steam { tabContents[Tab.PopularMods].Button.Enabled = false; } - GUIFrame listFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), content.RectTransform), style: null); + GUIFrame listFrame = new GUIFrame(new RectTransform(Vector2.One, content.RectTransform), style: null); CreateWorkshopItemList(listFrame, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); - new GUIButton(new RectTransform((1.0f, 0.05f), content.RectTransform, Anchor.BottomLeft), - style: "GUIButtonSmall", text: TextManager.Get("FindModsButton")) - { - OnClicked = (button, o) => - { - SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); - return false; - } - }; } private void CreatePublishTab(out GUIListBox selfModsList) @@ -160,6 +165,10 @@ namespace Barotrauma.Steam .Select(c => c.UserData as RegularPackage).OfType().ToArray()); PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); ContentPackageManager.LogEnabledRegularPackageErrors(); + enabledCoreDropdown.ButtonTextColor = + EnabledCorePackage.HasAnyErrors + ? GUIStyle.Red + : GUIStyle.TextColorNormal; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index 09bdaafe2..c3d167fed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -294,10 +294,11 @@ namespace Barotrauma.Steam { //Reload the package to force hash recalculation string packageName = localPackage.Name; - localPackage = ContentPackageManager.ReloadContentPackage(localPackage); - if (localPackage is null) + var result = ContentPackageManager.ReloadContentPackage(localPackage); + if (!result.TryUnwrapSuccess(out localPackage)) { - throw new Exception($"\"{packageName}\" was removed upon reload"); + throw new Exception($"\"{packageName}\" was removed upon reload", + result.TryUnwrapFailure(out var exception) ? exception : null); } //Set up the Ugc.Editor object that we'll need to publish diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index cbe5fca4d..d99e8887b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -133,20 +133,29 @@ namespace Barotrauma.Steam return searchBox; } - protected void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText) + protected static void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText) { - if (mod.Errors.Any()) + uiElement.ToolTip = ""; + if (mod.FatalLoadErrors.Any()) { const int maxErrorsToShow = 5; nameText.TextColor = GUIStyle.Red; uiElement.ToolTip = - TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) - + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message)); - if (mod.Errors.Count() > maxErrorsToShow) + TextManager.GetWithVariable("ContentPackageHasFatalErrors", "[packagename]", mod.Name) + + '\n' + string.Join('\n', mod.FatalLoadErrors.Take(maxErrorsToShow).Select(e => e.Message)); + if (mod.FatalLoadErrors.Length > maxErrorsToShow) { - uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString()); + uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.FatalLoadErrors.Count() - maxErrorsToShow).ToString()); } } + + if (mod.EnableError.IsSome()) + { + nameText.TextColor = GUIStyle.Red; + if (!uiElement.ToolTip.IsNullOrWhiteSpace()) { uiElement.ToolTip += "\n"; } + uiElement.ToolTip += TextManager.GetWithVariable( + "ContentPackageEnableError", "[packagename]", mod.Name); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 2d4a1ffb2..67bc81568 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -104,7 +104,7 @@ namespace Barotrauma } } - List successEffects = statusEffects.Where(se => se.type == ActionType.OnUse).ToList(); + List successEffects = statusEffects.Where(se => se.type == ActionType.OnSuccess).ToList(); List failureEffects = statusEffects.Where(se => se.type == ActionType.OnFailure).ToList(); foreach (StatusEffect statusEffect in successEffects) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 3d193471b..e04dfc070 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -512,5 +512,34 @@ namespace Barotrauma return new Vector2(paddingX, paddingY); } + + public static string ColorSectionOfString(string text, int start, int length, Color color) + { + int end = start + length; + + if (start < 0 || length < 0 || end > text.Length) + { + throw new ArgumentOutOfRangeException($"Invalid start ({start}) or length ({length}) for text \"{text}\"."); + } + + string stichedString = string.Empty; + + if (start > 0) + { + stichedString += text[..start]; + } + + // this is the highlighted part + stichedString += ColorString(text[start..end], color); + + if (end < text.Length) + { + stichedString += text[end..]; + } + + return stichedString; + + static string ColorString(string text, Color color) => $"‖color:{color.ToStringHex()}‖{text}‖end‖"; + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 18f018748..a527d2fba 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.14.0 + 0.20.12.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -106,6 +106,8 @@ PreserveNewest + + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index d8f0f351e..e4bcb6aab 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.14.0 + 0.20.12.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -106,6 +106,7 @@ PreserveNewest + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/libSDL2-2.0.so.0 b/Barotrauma/BarotraumaClient/libSDL2-2.0.so.0 index bdd69273a..72cda07f7 100644 Binary files a/Barotrauma/BarotraumaClient/libSDL2-2.0.so.0 and b/Barotrauma/BarotraumaClient/libSDL2-2.0.so.0 differ diff --git a/Barotrauma/BarotraumaClient/x64/SDL2.dll b/Barotrauma/BarotraumaClient/x64/SDL2.dll index 6cf858caa..8c6230e6f 100644 Binary files a/Barotrauma/BarotraumaClient/x64/SDL2.dll and b/Barotrauma/BarotraumaClient/x64/SDL2.dll differ diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index d9fec7716..bd4a7db81 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.14.0 + 0.20.12.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 1513d1250..7ef8e2299 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.14.0 + 0.20.12.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 49893f4dd..5aca9fbf7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -70,7 +70,7 @@ namespace Barotrauma msg.WriteByte((byte)Job.Variant); foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) { - msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier).Level); + msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); } } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a6357e6ed..92d94d304 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -219,9 +219,9 @@ namespace Barotrauma else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) { #if DEBUG || UNSTABLE - DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); + DebugConsole.AddWarning($"Large discrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); #endif - LastNetworkUpdateID = networkUpdateID; + LastNetworkUpdateID = LastProcessedID = networkUpdateID; } if (memInput.Count > 60) { @@ -269,10 +269,13 @@ namespace Barotrauma case EventType.UpdateTalents: if (c.Character != this) { + if (!IsBot || !c.HasPermission(ClientPermissions.ManageBotTalents)) + { #if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - return; + return; + } } // get the full list of talents from the player, only give the ones @@ -553,7 +556,7 @@ namespace Barotrauma msg.WriteByte((byte)statType); foreach (var savedStatValue in Info.SavedStatValues[statType]) { - msg.WriteString(savedStatValue.StatIdentifier); + msg.WriteIdentifier(savedStatValue.StatIdentifier); msg.WriteSingle(savedStatValue.StatValue); msg.WriteBoolean(savedStatValue.RemoveOnDeath); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index a1e2ba10c..ceaf4ab10 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -83,54 +83,50 @@ namespace Barotrauma } //dequeue messages - lock (queuedMessages) + if (queuedMessages.Count > 0) { - if (queuedMessages.Count > 0) + + if (!Console.IsOutputRedirected) { - - if (!Console.IsOutputRedirected) + Console.CursorLeft = 0; + } + while (queuedMessages.TryDequeue(out var msg)) + { + Messages.Add(msg); + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { - Console.CursorLeft = 0; - } - while (queuedMessages.Count > 0) - { - ColoredText msg = queuedMessages.Dequeue(); - Messages.Add(msg); - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + unsavedMessages.Add(msg); + if (unsavedMessages.Count >= messagesPerFile) { - unsavedMessages.Add(msg); - if (unsavedMessages.Count >= messagesPerFile) - { - SaveLogs(); - unsavedMessages.Clear(); - } + SaveLogs(); + unsavedMessages.Clear(); } - - string msgTxt = msg.Text; - - if (msg.IsCommand) commandMemory.Add(msgTxt); - - if(!Console.IsOutputRedirected) - { - int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1; - msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0); - - Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); - } - Console.WriteLine(msgTxt); - - if (sw.ElapsedMilliseconds >= maxTime) { break; } } + + string msgTxt = msg.Text; + + if (msg.IsCommand) commandMemory.Add(msgTxt); + if(!Console.IsOutputRedirected) { - RewriteInputToCommandLine(input); + int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1; + msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0); + + Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); } + Console.WriteLine(msgTxt); + + if (sw.ElapsedMilliseconds >= maxTime) { break; } } - if (Messages.Count > MaxMessages) + if (!Console.IsOutputRedirected) { - Messages.RemoveRange(0, Messages.Count - MaxMessages); + RewriteInputToCommandLine(input); } } + if (Messages.Count > MaxMessages) + { + Messages.RemoveRange(0, Messages.Count - MaxMessages); + } // No good way to display input when console output is redirected, and can't read from redirected input using KeyAvailable. if(!Console.IsOutputRedirected && !Console.IsInputRedirected) @@ -273,26 +269,22 @@ namespace Barotrauma public static void Clear() { - lock (queuedMessages) + while (queuedMessages.TryDequeue(out var msg)) { - while (queuedMessages.Count > 0) + Messages.Add(msg); + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { - var msg = queuedMessages.Dequeue(); - Messages.Add(msg); - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + unsavedMessages.Add(msg); + if (unsavedMessages.Count >= messagesPerFile) { - unsavedMessages.Add(msg); - if (unsavedMessages.Count >= messagesPerFile) - { - SaveLogs(); - unsavedMessages.Clear(); - } + SaveLogs(); + unsavedMessages.Clear(); } } - if (Messages.Count > MaxMessages) - { - Messages.RemoveRange(0, Messages.Count - MaxMessages); - } + } + if (Messages.Count > MaxMessages) + { + Messages.RemoveRange(0, Messages.Count - MaxMessages); } } @@ -1442,6 +1434,21 @@ namespace Barotrauma GameMain.Server.PrintSenderTransters(); })); + + AssignOnExecute("resetcharacternetstate", (string[] args) => + { + if (GameMain.Server == null) { return; } + + if (args.Length < 1) + { + ThrowError("Invalid parameters. The command should be formatted as \"resetcharacternetstate [character]\". If the names consist of multiple words, you should surround them with quotation marks."); + return; + } + + var character = FindMatchingCharacter(args.Skip(1).ToArray(), false); + character?.ResetNetState(); + }); + commands.Add(new Command("eventdata", "", (string[] args) => { if (args.Length == 0) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index dfa4bc4c0..5d9dde87c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -31,7 +31,7 @@ namespace Barotrauma if (convAction.SelectedOption > -1) { //someone else already chose an option for this conversation: interrupt for this client - convAction.ServerWrite(convAction.speaker, sender, interrupt: true); + convAction.ServerWrite(convAction.Speaker, sender, interrupt: true); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index e931874c2..6ef5e5dc9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -165,17 +165,17 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); if (doc?.Root == null) { - DebugConsole.ThrowError("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings."); + DebugConsole.AddWarning("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings."); } else { - name = doc.Root.GetAttributeString("name", "Server"); - port = doc.Root.GetAttributeInt("port", NetConfig.DefaultPort); - queryPort = doc.Root.GetAttributeInt("queryport", NetConfig.DefaultQueryPort); - publiclyVisible = doc.Root.GetAttributeBool("public", false); + name = doc.Root.GetAttributeString(nameof(ServerSettings.Name), "Server"); + port = doc.Root.GetAttributeInt(nameof(ServerSettings.Port), NetConfig.DefaultPort); + queryPort = doc.Root.GetAttributeInt(nameof(ServerSettings.QueryPort), NetConfig.DefaultQueryPort); + publiclyVisible = doc.Root.GetAttributeBool(nameof(ServerSettings.IsPublic), false); + enableUpnp = doc.Root.GetAttributeBool(nameof(ServerSettings.EnableUPnP), false); + maxPlayers = doc.Root.GetAttributeInt(nameof(ServerSettings.MaxPlayers), 10); password = doc.Root.GetAttributeString("password", ""); - enableUpnp = doc.Root.GetAttributeBool("enableupnp", false); - maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10); ownerKey = Option.None(); } @@ -386,7 +386,7 @@ namespace Barotrauma if (prevUpdateRates.Count >= 10) { int avgUpdateRate = (int)prevUpdateRates.Average(); - if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && Timing.TotalTime > GameSession.RoundStartTime + 1.0) + if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && GameSession.RoundDuration > 1.0) { DebugConsole.AddWarning($"Running slowly ({avgUpdateRate} updates/s)!"); if (Server != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 52ef55b2f..4283c7206 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -23,10 +23,8 @@ namespace Barotrauma return client.HasPermission(permissions) || client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Server.ConnectedClients.Count == 1 || IsOwner(client) || - //allow managing if no-one with permissions is alive - GameMain.Server.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + AnyOneAllowedToManageCampaign(permissions); } public bool AllowedToManageWallets(Client client) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 12d7a7af3..6d879caf7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -335,7 +335,7 @@ namespace Barotrauma break; } - Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); if (success) @@ -347,6 +347,8 @@ namespace Barotrauma (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c); } } + // Event history must be registered before ending the round or it will be cleared + GameMain.GameSession.EventManager.RegisterEventHistory(); } GameMain.GameSession.EndRound("", traitorResults, transitionType); @@ -360,7 +362,6 @@ namespace Barotrauma LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - GameMain.GameSession.EventManager.RegisterEventHistory(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e30b1148f..e46134ca6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma @@ -30,6 +32,9 @@ namespace Barotrauma switch (header) { + case NetworkHeader.ADD_EVERYTHING_TO_PENDING: + ProcessAddEverything(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -57,7 +62,14 @@ namespace Barotrauma NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); - ServerSend(newCrewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + ServerSend(new NetCollection(newCrewMember), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessAddEverything(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + AddEverythingToPending(); + ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessNewRemoval(IReadMessage inc, Client client) @@ -73,12 +85,7 @@ namespace Barotrauma { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } - INetSerializableStruct writeCrewMember = new NetPendingCrew - { - CrewMembers = PendingHeals.ToArray() - }; - - ServerSend(writeCrewMember, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); + ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) @@ -107,10 +114,10 @@ namespace Barotrauma CharacterInfo? foundInfo = crewMember.FindCharacterInfo(GetCrewCharacters()); - NetAffliction[] pendingAfflictions = Array.Empty(); + ImmutableArray pendingAfflictions = ImmutableArray.Empty; int infoId = 0; - if (foundInfo is { Character: { CharacterHealth: { } health } }) + if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); infoId = foundInfo.GetIdentifierUsingOriginalName(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index c3a383431..8a99abc2a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -9,11 +9,12 @@ namespace Barotrauma.Items.Components public void ServerEventRead(IReadMessage msg, Client c) { uint recipeHash = msg.ReadUInt32(); - + int amountToFabricate = msg.ReadRangedInteger(1, MaxAmountToFabricate); item.CreateServerEvent(this); if (!item.CanClientAccess(c)) { return; } + AmountToFabricate = amountToFabricate; if (recipeHash == 0) { CancelFabricating(c.Character); @@ -24,6 +25,8 @@ namespace Barotrauma.Items.Components if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; } if (recipeHash == 0) { return; } + amountRemaining = AmountToFabricate; + StartFabricating(fabricationRecipes[recipeHash], c.Character); } } @@ -56,6 +59,8 @@ namespace Barotrauma.Items.Components { var componentData = ExtractEventData(extraData); msg.WriteByte((byte)componentData.State); + msg.WriteRangedInteger(AmountToFabricate, 0, MaxAmountToFabricate); + msg.WriteRangedInteger(amountRemaining, 0, MaxAmountToFabricate); msg.WriteSingle(timeUntilReady); uint recipeHash = fabricatedItem?.RecipeHash ?? 0; msg.WriteUInt32(recipeHash); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs index 0cfbe9026..c9cad1019 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs @@ -18,20 +18,22 @@ namespace Barotrauma.Items.Components bool powerOn = msg.ReadBoolean(); float fissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); float turbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); + float temperatureBoostAmount = msg.ReadRangedSingle(-TemperatureBoostAmount, TemperatureBoostAmount, 8); if (!item.CanClientAccess(c)) { return; } IsActive = true; - if (!autoTemp && AutoTemp) blameOnBroken = c; - if (turbineOutput < TargetTurbineOutput) blameOnBroken = c; - if (fissionRate > TargetFissionRate) blameOnBroken = c; - if (!_powerOn && powerOn) blameOnBroken = c; + if (!autoTemp && AutoTemp) { blameOnBroken = c; } + if (turbineOutput < TargetTurbineOutput) { blameOnBroken = c; } + if (fissionRate > TargetFissionRate) { blameOnBroken = c; } + if (!_powerOn && powerOn) { blameOnBroken = c; } AutoTemp = autoTemp; _powerOn = powerOn; TargetFissionRate = fissionRate; TargetTurbineOutput = turbineOutput; + if (AllowTemperatureBoost) { temperatureBoost = temperatureBoostAmount; } LastUser = c.Character; if (nextServerLogWriteTime == null) @@ -51,6 +53,7 @@ namespace Barotrauma.Items.Components msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); msg.WriteRangedSingle(degreeOfSuccess, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(temperatureBoost, -TemperatureBoostAmount, TemperatureBoostAmount, 8); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs index e0cf10812..5529c4af3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); - float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); + float chargeRatio = MathHelper.Clamp(charge / adjustedCapacity, 0.0f, 1.0f); msg.WriteRangedSingle(chargeRatio, 0.0f, 1.0f, 8); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 862478ce6..3dc30f6cf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -106,6 +106,14 @@ namespace Barotrauma $"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})"); } break; + case SetItemStatEventData setItemStatEventData: + msg.WriteByte((byte)setItemStatEventData.Stats.Count); + foreach (var (key, value) in setItemStatEventData.Stats) + { + msg.WriteNetSerializableStruct(key); + msg.WriteSingle(value); + } + break; case UpgradeEventData upgradeEventData: var upgrade = upgradeEventData.Upgrade; var upgradeTargets = upgrade.TargetComponents; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 15f4d365a..9febcddaf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -152,7 +152,9 @@ namespace Barotrauma.Networking public bool IsBanned(AccountId accountId, out string reason) { RemoveExpired(); - var bannedPlayer = bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)); + var bannedPlayer = + bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)) ?? + bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out Address adr) && adr is SteamP2PAddress steamAdr && steamAdr.SteamId.Equals(accountId)); reason = bannedPlayer?.Reason ?? string.Empty; return bannedPlayer != null; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 0fb07b70a..1b45ea618 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -210,9 +210,9 @@ namespace Barotrauma.Networking return length; } - public virtual void ServerWrite(IWriteMessage msg, Client c) + public virtual void ServerWrite(in SegmentTableWriter segmentTable, IWriteMessage msg, Client c) { - msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); + segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteByte((byte)ChangeType); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index fdea971a5..c6d6559cc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -40,6 +40,8 @@ namespace Barotrauma.Networking public float ChatSpamTimer; public int ChatSpamCount; + public string RejectedName; + public int RoundsSincePlayedAsTraitor; public float KickAFKTimer; @@ -69,6 +71,9 @@ namespace Barotrauma.Networking public DateTime JoinTime; + public static readonly TimeSpan NameChangeCoolDown = new TimeSpan(hours: 0, minutes: 0, seconds: 30); + public DateTime LastNameChangeTime; + private CharacterInfo characterInfo; public CharacterInfo CharacterInfo { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 19b84712c..fa092aaa9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -342,9 +342,9 @@ namespace Barotrauma.Networking #endif if (!started) { return; } - if (OwnerConnection != null && ChildServerRelay.HasShutDown) + if (ChildServerRelay.HasShutDown) { - Quit(); + GameMain.Instance.CloseServer(); return; } @@ -1041,12 +1041,11 @@ namespace Barotrauma.Networking return; } - ClientNetObject objHeader; - while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE) + SegmentTableReader.Read(inc, (segment, inc) => { - switch (objHeader) + switch (segment) { - case ClientNetObject.SYNC_IDS: + case ClientNetSegment.SyncIds: //TODO: might want to use a clever class for this c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID); if (c.HasPermission(ClientPermissions.ManageSettings) && @@ -1085,19 +1084,21 @@ namespace Barotrauma.Networking } } break; - case ClientNetObject.CHAT_MESSAGE: + case ClientNetSegment.ChatMessage: ChatMessage.ServerRead(inc, c); break; - case ClientNetObject.VOTE: + case ClientNetSegment.Vote: Voting.ServerRead(inc, c); break; default: - return; + return SegmentTableReader.BreakSegmentReading.Yes; } //don't read further messages if the client has been disconnected (kicked due to spam for example) - if (!connectedClients.Contains(c)) break; - } + return connectedClients.Contains(c) + ? SegmentTableReader.BreakSegmentReading.No + : SegmentTableReader.BreakSegmentReading.Yes; + }); } private void ClientReadIngame(IReadMessage inc) @@ -1122,13 +1123,12 @@ namespace Barotrauma.Networking } } - ClientNetObject objHeader; - while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE) + SegmentTableReader.Read(inc, (segment, inc) => { - switch (objHeader) + switch (segment) { - case ClientNetObject.SYNC_IDS: - //TODO: might want to use a clever class for this + case ClientNetSegment.SyncIds: + //TODO: switch this to INetSerializableStruct UInt16 lastRecvChatMsgID = inc.ReadUInt16(); UInt16 lastRecvEntityEventID = inc.ReadUInt16(); @@ -1231,10 +1231,10 @@ namespace Barotrauma.Networking } break; - case ClientNetObject.CHAT_MESSAGE: + case ClientNetSegment.ChatMessage: ChatMessage.ServerRead(inc, c); break; - case ClientNetObject.CHARACTER_INPUT: + case ClientNetSegment.CharacterInput: if (c.Character != null) { c.Character.ServerReadInput(inc, c); @@ -1244,22 +1244,24 @@ namespace Barotrauma.Networking DebugConsole.AddWarning($"Received character inputs from a client who's not controlling a character ({c.Name})."); } break; - case ClientNetObject.ENTITY_STATE: + case ClientNetSegment.EntityState: entityEventManager.Read(inc, c); break; - case ClientNetObject.VOTE: + case ClientNetSegment.Vote: Voting.ServerRead(inc, c); break; - case ClientNetObject.SPECTATING_POS: + case ClientNetSegment.SpectatingPos: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); break; default: - return; + return SegmentTableReader.BreakSegmentReading.Yes; } //don't read further messages if the client has been disconnected (kicked due to spam for example) - if (!connectedClients.Contains(c)) { break; } - } + return connectedClients.Contains(c) + ? SegmentTableReader.BreakSegmentReading.No + : SegmentTableReader.BreakSegmentReading.Yes; + }); } private void ReadCrewMessage(IReadMessage inc, Client sender) @@ -1410,7 +1412,8 @@ namespace Barotrauma.Networking if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + mpCampaign.UpdateStoreStock(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -1714,78 +1717,78 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.WriteSingle((float)NetTime.Now); - outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); - outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server - outmsg.WriteUInt16(c.LastSentEntityEventID); - - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - outmsg.WriteBoolean(true); - outmsg.WritePadBits(); - campaign.ServerWrite(outmsg, c); - } - else - { - outmsg.WriteBoolean(false); - outmsg.WritePadBits(); - } + segmentTable.StartNewSegment(ServerNetSegment.SyncIds); + outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server + outmsg.WriteUInt16(c.LastSentEntityEventID); - int clientListBytes = outmsg.LengthBytes; - WriteClientList(c, outmsg); - clientListBytes = outmsg.LengthBytes - clientListBytes; - - int chatMessageBytes = outmsg.LengthBytes; - WriteChatMessages(outmsg, c); - chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; - - //write as many position updates as the message can fit (only after midround syncing is done) - int positionUpdateBytes = outmsg.LengthBytes; - while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) - { - var entity = c.PendingPositionUpdates.Peek(); - if (!(entity is IServerPositionSync entityPositionSync) || - entity.Removed || - (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { + outmsg.WriteBoolean(true); + outmsg.WritePadBits(); + campaign.ServerWrite(outmsg, c); + } + else + { + outmsg.WriteBoolean(false); + outmsg.WritePadBits(); + } + + int clientListBytes = outmsg.LengthBytes; + WriteClientList(segmentTable, c, outmsg); + clientListBytes = outmsg.LengthBytes - clientListBytes; + + int chatMessageBytes = outmsg.LengthBytes; + WriteChatMessages(segmentTable, outmsg, c); + chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; + + //write as many position updates as the message can fit (only after midround syncing is done) + int positionUpdateBytes = outmsg.LengthBytes; + while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) + { + var entity = c.PendingPositionUpdates.Peek(); + if (!(entity is IServerPositionSync entityPositionSync) || + entity.Removed || + (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) + { + c.PendingPositionUpdates.Dequeue(); + continue; + } + + IWriteMessage tempBuffer = new ReadWriteMessage(); + tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); + tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); + entityPositionSync.ServerWritePosition(tempBuffer, c); + + //no more room in this packet + if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) + { + break; + } + + segmentTable.StartNewSegment(ServerNetSegment.EntityPosition); + outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + outmsg.WritePadBits(); + + c.PositionUpdateLastSent[entity] = (float)NetTime.Now; c.PendingPositionUpdates.Dequeue(); - continue; } + positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; - IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); - tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); - entityPositionSync.ServerWritePosition(tempBuffer, c); - - //no more room in this packet - if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) + if (outmsg.LengthBytes > MsgConstants.MTU) { - break; + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; + errorMsg += + " Client list size: " + clientListBytes + " bytes\n" + + " Chat message size: " + chatMessageBytes + " bytes\n" + + " Position update size: " + positionUpdateBytes + " bytes\n\n"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } - - outmsg.WriteByte((byte)ServerNetObject.ENTITY_POSITION); - outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); - outmsg.WritePadBits(); - - c.PositionUpdateLastSent[entity] = (float)NetTime.Now; - c.PendingPositionUpdates.Dequeue(); - } - positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; - - outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - - if (outmsg.LengthBytes > MsgConstants.MTU) - { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - errorMsg += - " Client list size: " + clientListBytes + " bytes\n" + - " Chat message size: " + chatMessageBytes + " bytes\n" + - " Position update size: " + positionUpdateBytes + " bytes\n\n"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); @@ -1798,46 +1801,50 @@ namespace Barotrauma.Networking outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now); - int eventManagerBytes = outmsg.LengthBytes; - entityEventManager.Write(c, outmsg, out List sentEvents); - eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; - - if (sentEvents.Count == 0) + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - break; - } + int eventManagerBytes = outmsg.LengthBytes; + entityEventManager.Write(segmentTable, c, outmsg, out List sentEvents); + eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; - outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - - if (outmsg.LengthBytes > MsgConstants.MTU) - { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - errorMsg += - " Event size: " + eventManagerBytes + " bytes\n"; - - if (sentEvents != null && sentEvents.Count > 0) + if (sentEvents.Count == 0) { - errorMsg += "Sent events: \n"; - foreach (var entityEvent in sentEvents) - { - errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; - } + break; } - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + if (outmsg.LengthBytes > MsgConstants.MTU) + { + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + + MsgConstants.MTU + ")\n"; + errorMsg += + " Event size: " + eventManagerBytes + " bytes\n"; + + if (sentEvents != null && sentEvents.Count > 0) + { + errorMsg += "Sent events: \n"; + foreach (var entityEvent in sentEvents) + { + errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; + } + } + + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } - private void WriteClientList(Client c, IWriteMessage outmsg) + private void WriteClientList(in SegmentTableWriter segmentTable, Client c, IWriteMessage outmsg) { bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); if (!hasChanged) { return; } - outmsg.WriteByte((byte)ServerNetObject.CLIENT_LIST); + segmentTable.StartNewSegment(ServerNetSegment.ClientList); outmsg.WriteUInt16(LastClientListUpdateID); GameMain.LuaCs.Hook.Call("writeClientList", c, outmsg); @@ -1883,133 +1890,135 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); - outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); - - int settingsBytes = outmsg.LengthBytes; - int initialUpdateBytes = 0; - - if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None) + bool messageTooLarge; + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - GameMain.NetLobbyScreen.LastUpdateID++; - } - - IWriteMessage settingsBuf = null; - if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) - { - outmsg.WriteBoolean(true); - outmsg.WritePadBits(); + segmentTable.StartNewSegment(ServerNetSegment.SyncIds); - outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + int settingsBytes = outmsg.LengthBytes; + int initialUpdateBytes = 0; - settingsBuf = new ReadWriteMessage(); - ServerSettings.ServerWrite(settingsBuf, c); - outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); - outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); - - outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1); - if (c.LastRecvLobbyUpdate < 1) + if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None) { - isInitialUpdate = true; - initialUpdateBytes = outmsg.LengthBytes; - ClientWriteInitial(c, outmsg); - initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; + GameMain.NetLobbyScreen.LastUpdateID++; } - outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); - outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); - outmsg.WriteBoolean(IsUsingRespawnShuttle()); - var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? - RespawnManager.RespawnShuttle.Info : - GameMain.NetLobbyScreen.SelectedShuttle; - outmsg.WriteString(selectedShuttle.Name); - outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); - outmsg.WriteBoolean(ServerSettings.AllowSubVoting); - outmsg.WriteBoolean(ServerSettings.AllowModeVoting); - - outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled); - - outmsg.WriteBoolean(ServerSettings.AllowSpectating); - - outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); - - outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); - - outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); - outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); - outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty); - - outmsg.WriteByte((byte)ServerSettings.BotCount); - outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill); - - outmsg.WriteBoolean(ServerSettings.AutoRestart); - if (ServerSettings.AutoRestart) + IWriteMessage settingsBuf = null; + if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) { - outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); + outmsg.WriteBoolean(true); + outmsg.WritePadBits(); + + outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + + settingsBuf = new ReadWriteMessage(); + ServerSettings.ServerWrite(settingsBuf, c); + outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); + outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); + + outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1); + if (c.LastRecvLobbyUpdate < 1) + { + isInitialUpdate = true; + initialUpdateBytes = outmsg.LengthBytes; + ClientWriteInitial(c, outmsg); + initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; + } + outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); + outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); + outmsg.WriteBoolean(IsUsingRespawnShuttle()); + var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? + RespawnManager.RespawnShuttle.Info : + GameMain.NetLobbyScreen.SelectedShuttle; + outmsg.WriteString(selectedShuttle.Name); + outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); + + outmsg.WriteBoolean(ServerSettings.AllowSubVoting); + outmsg.WriteBoolean(ServerSettings.AllowModeVoting); + + outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled); + + outmsg.WriteBoolean(ServerSettings.AllowSpectating); + + outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); + + outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); + + outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); + outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); + outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty); + + outmsg.WriteByte((byte)ServerSettings.BotCount); + outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill); + + outmsg.WriteBoolean(ServerSettings.AutoRestart); + if (ServerSettings.AutoRestart) + { + outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); + } } - } - else - { - outmsg.WriteBoolean(false); - outmsg.WritePadBits(); - } - settingsBytes = outmsg.LengthBytes - settingsBytes; - - int campaignBytes = outmsg.LengthBytes; - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) - { - outmsg.WriteBoolean(true); - outmsg.WritePadBits(); - campaign.ServerWrite(outmsg, c); - } - else - { - outmsg.WriteBoolean(false); - outmsg.WritePadBits(); - } - campaignBytes = outmsg.LengthBytes - campaignBytes; - - outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server - - int clientListBytes = outmsg.LengthBytes; - if (outmsg.LengthBytes < MsgConstants.MTU - 500) - { - WriteClientList(c, outmsg); - } - clientListBytes = outmsg.LengthBytes - clientListBytes; - - int chatMessageBytes = outmsg.LengthBytes; - WriteChatMessages(outmsg, c); - chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; - - outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - - bool messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU; - if (messageTooLarge && !isInitialUpdate) - { - string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - warningMsg += - " Client list size: " + clientListBytes + " bytes\n" + - " Chat message size: " + chatMessageBytes + " bytes\n" + - " Campaign size: " + campaignBytes + " bytes\n" + - " Settings size: " + settingsBytes + " bytes\n"; - if (initialUpdateBytes > 0) + else { + outmsg.WriteBoolean(false); + outmsg.WritePadBits(); + } + settingsBytes = outmsg.LengthBytes - settingsBytes; + + int campaignBytes = outmsg.LengthBytes; + var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; + if (outmsg.LengthBytes < MsgConstants.MTU - 500 && + campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + { + outmsg.WriteBoolean(true); + outmsg.WritePadBits(); + campaign.ServerWrite(outmsg, c); + } + else + { + outmsg.WriteBoolean(false); + outmsg.WritePadBits(); + } + campaignBytes = outmsg.LengthBytes - campaignBytes; + + outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server + + int clientListBytes = outmsg.LengthBytes; + if (outmsg.LengthBytes < MsgConstants.MTU - 500) + { + WriteClientList(segmentTable, c, outmsg); + } + clientListBytes = outmsg.LengthBytes - clientListBytes; + + int chatMessageBytes = outmsg.LengthBytes; + WriteChatMessages(segmentTable, outmsg, c); + chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; + + messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU; + if (messageTooLarge && !isInitialUpdate) + { + string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; warningMsg += - " Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; - } - if (settingsBuf != null) - { - warningMsg += - " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; - } + " Client list size: " + clientListBytes + " bytes\n" + + " Chat message size: " + chatMessageBytes + " bytes\n" + + " Campaign size: " + campaignBytes + " bytes\n" + + " Settings size: " + settingsBytes + " bytes\n"; + if (initialUpdateBytes > 0) + { + warningMsg += + " Initial update size: " + initialUpdateBytes + " bytes\n"; + } + if (settingsBuf != null) + { + warningMsg += + " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; + } #if DEBUG || UNSTABLE - DebugConsole.ThrowError(warningMsg); + DebugConsole.ThrowError(warningMsg); #else - if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } #endif - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } } if (isInitialUpdate || messageTooLarge) @@ -2036,7 +2045,7 @@ namespace Barotrauma.Networking } } - private void WriteChatMessages(IWriteMessage outmsg, Client c) + private void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2046,7 +2055,7 @@ namespace Barotrauma.Networking //not enough room in this packet return; } - c.ChatMsgQueue[i].ServerWrite(outmsg, c); + c.ChatMsgQueue[i].ServerWrite(segmentTable, outmsg, c); } } @@ -2543,6 +2552,7 @@ namespace Barotrauma.Networking msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteBoolean(IsUsingRespawnShuttle()); msg.WriteByte((byte)ServerSettings.LosMode); + msg.WriteByte((byte)ServerSettings.ShowEnemyHealthBars); msg.WriteBoolean(includesFinalize); msg.WritePadBits(); ServerSettings.WriteMonsterEnabled(msg); @@ -2746,9 +2756,24 @@ namespace Barotrauma.Networking CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; } + + var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime; + if (timeSinceNameChange < Client.NameChangeCoolDown) + { + //only send once per second at most to prevent using this for spamming + if (timeSinceNameChange.TotalSeconds > 1) + { + var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; + SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); + } + c.NameId = nameId; + c.RejectedName = newName; + return false; + } + if (!newJob.IsEmpty) { - if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob) + if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob) { newJob = Identifier.Empty; } @@ -2773,26 +2798,25 @@ namespace Barotrauma.Networking public bool TryChangeClientName(Client c, string newName) { newName = Client.SanitizeName(newName); - //update client list even if the name cannot be changed to the one sent by the client, - //so the client will be informed what their actual name is - LastClientListUpdateID++; - - if (newName == c.Name || string.IsNullOrEmpty(newName)) { return false; } - - if (IsNameValid(c, newName)) + if (newName != c.Name && !string.IsNullOrEmpty(newName) && IsNameValid(c, newName)) { + c.LastNameChangeTime = DateTime.Now; string oldName = c.Name; c.Name = newName; + c.RejectedName = string.Empty; SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); + LastClientListUpdateID++; return true; } else { + //update client list even if the name cannot be changed to the one sent by the client, + //so the client will be informed what their actual name is + LastClientListUpdateID++; return false; } } - private bool IsNameValid(Client c, string newName) { newName = Client.SanitizeName(newName); @@ -3365,9 +3389,11 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); - msg.WriteByte((byte)ServerNetObject.VOTE); - Voting.ServerWrite(msg); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + using (var segmentTable = SegmentTableWriter.StartWriting(msg)) + { + segmentTable.StartNewSegment(ServerNetSegment.Vote); + Voting.ServerWrite(msg); + } foreach (var c in recipients) { @@ -3971,6 +3997,7 @@ namespace Barotrauma.Networking public void Quit() { + if (started) { started = false; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index abcbbf42c..6bf35ee39 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -123,7 +123,7 @@ 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 (Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) + if (GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) { events.RemoveAll(e => (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && @@ -168,9 +168,10 @@ namespace Barotrauma.Networking if (!bufferedEvent.Character.IsIncapacitated && NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID)) { + DebugConsole.Log($"Delaying reading entity event sent by a client until the character state has been processed. Event's character state: {bufferedEvent.CharacterStateID}, last processed character state: {bufferedEvent.Character.LastProcessedID}"); continue; } - + try { ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender); @@ -216,7 +217,7 @@ namespace Barotrauma.Networking if (Timing.TotalTime - lastWarningTime > 5.0 && Timing.TotalTime - lastSentToAnyoneTime > 10.0 && - Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) + GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage); @@ -228,7 +229,7 @@ namespace Barotrauma.Networking ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1)); if (firstEventToResend != null && - Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration && + GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration && ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime)) { // This event is 10 seconds older than the last one we've successfully sent, @@ -294,15 +295,15 @@ namespace Barotrauma.Networking /// /// Writes all the events that the client hasn't received yet into the outgoing message /// - public void Write(Client client, IWriteMessage msg) + public void Write(in SegmentTableWriter segmentTable, Client client, IWriteMessage msg) { - Write(client, msg, out _); + Write(segmentTable, client, msg, out _); } /// /// Writes all the events that the client hasn't received yet into the outgoing message /// - public void Write(Client client, IWriteMessage msg, out List sentEvents) + public void Write(in SegmentTableWriter segmentTable, Client client, IWriteMessage msg, out List sentEvents) { List eventsToSync = GetEventsToSync(client); @@ -314,7 +315,7 @@ namespace Barotrauma.Networking //too many events for one packet //(normal right after a round has just started, don't show a warning if it's been less than 10 seconds) - if (eventsToSync.Count > 200 && GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 10.0) + if (eventsToSync.Count > 200 && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0) { if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0) { @@ -344,7 +345,7 @@ namespace Barotrauma.Networking if (client.NeedsMidRoundSync) { - msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT_INITIAL); + segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial); msg.WriteUInt16(client.UnreceivedEntityEventCount); msg.WriteUInt16(client.FirstNewEventID); @@ -352,7 +353,7 @@ namespace Barotrauma.Networking } else { - msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT); + segmentTable.StartNewSegment(ServerNetSegment.EntityEvent); Write(msg, eventsToSync, out sentEvents, client); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index 21d3f9ae8..a02d01ebe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,13 +1,13 @@ -using Barotrauma.Steam; +using Barotrauma.Steam; using System; namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { - public override void ServerWrite(IWriteMessage msg, Client c) + public override void ServerWrite(in SegmentTableWriter segmentTable, IWriteMessage msg, Client c) { - msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); + segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteString(SenderName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 0a6a78003..04a06742f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -255,7 +255,9 @@ namespace Barotrauma.Networking structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, - ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) + ContentPackages = ContentPackageManager.EnabledPackages.All + .Where(cp => cp.Files.Any()) + .Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .ToImmutableArray() }; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 817c19b59..5ad450e01 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -47,7 +47,7 @@ namespace Barotrauma.Networking } } - private bool IsRespawnPromptPendingForClient(Client c) + private static bool IsRespawnPromptPendingForClient(Client c) { if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; } @@ -70,7 +70,7 @@ namespace Barotrauma.Networking return false; } - private List GetBotsToRespawn() + private static List GetBotsToRespawn() { if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal) { @@ -113,7 +113,7 @@ namespace Barotrauma.Networking return ShouldStartRespawnCountdown(characterToRespawnCount); } - private int GetMinCharactersToRespawn() + private static int GetMinCharactersToRespawn() { return Math.Max((int)(GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio), 1); } @@ -485,7 +485,7 @@ namespace Barotrauma.Networking } } - if (!(GameMain.GameSession.GameMode is CampaignMode)) + if (GameMain.GameSession.GameMode is not CampaignMode) { if (scooterPrefab != null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index a95387e2e..d4777ea28 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -272,7 +272,6 @@ namespace Barotrauma.Networking XDocument doc = new XDocument(new XElement("serversettings")); doc.Root.SetAttributeValue("name", ServerName); - doc.Root.SetAttributeValue("public", IsPublic); doc.Root.SetAttributeValue("port", Port); #if USE_STEAM doc.Root.SetAttributeValue("queryport", QueryPort); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index 33fa6dfad..df0eeebac 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -2,20 +2,18 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Text; -using MoonSharp.Interpreter; namespace Barotrauma.Networking { class VoipServer { - private ServerPeer netServer; - private List queues; - private Dictionary lastSendTime; + private readonly ServerPeer netServer; + private readonly List queues; + private readonly Dictionary lastSendTime; public VoipServer(ServerPeer server) { - this.netServer = server; + netServer = server; queues = new List(); lastSendTime = new Dictionary(); } @@ -47,17 +45,19 @@ namespace Barotrauma.Networking } Client sender = clients.Find(c => c.VoipQueue == queue); + if (sender == null) { return; } foreach (Client recipient in clients) { if (recipient == sender) { continue; } - if (!CanReceive(sender, recipient)) { continue; } + if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; } IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.VOICE); msg.WriteByte((byte)queue.QueueID); + msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8); queue.Write(msg); netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable); @@ -65,9 +65,15 @@ namespace Barotrauma.Networking } } - private bool CanReceive(Client sender, Client recipient) + private static bool CanReceive(Client sender, Client recipient, out float distanceFactor) { - if (Screen.Selected != GameMain.GameScreen) { return true; } + if (Screen.Selected != GameMain.GameScreen) + { + distanceFactor = 0.0f; + return true; + } + + distanceFactor = 0.0f; //no-one can hear muted players if (sender.Muted) { return false; } @@ -75,42 +81,46 @@ namespace Barotrauma.Networking bool recipientSpectating = recipient.Character == null || recipient.Character.IsDead; bool senderSpectating = sender.Character == null || sender.Character.IsDead; - //TODO: only allow spectators to hear the voice chat if close enough to the speaker? - - //non-spectators cannot hear spectators - if (senderSpectating && !recipientSpectating) { return false; } - - //both spectating, no need to do radio/distance checks - if (recipientSpectating && senderSpectating) { return true; } - - //spectators can hear non-spectators - if (!senderSpectating && recipientSpectating) { return true; } + //non-spectators cannot hear spectators, and spectators can always hear spectators + if (senderSpectating) + { + return recipientSpectating; + } //sender can't speak if (sender.Character != null && sender.Character.SpeechImpediment >= 100.0f) { return false; } //check if the message can be sent via radio + WifiComponent recipientRadio = null; if (!sender.VoipQueue.ForceLocal && - ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && - ChatMessage.CanUseRadio(recipient.Character, out WifiComponent recipientRadio)) + ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && + (recipientSpectating || ChatMessage.CanUseRadio(recipient.Character, out recipientRadio))) { - var should = GameMain.LuaCs.Hook.Call("canUseVoiceRadio", new object[] { sender, recipient }); + var canUse = GameMain.LuaCs.Hook.Call("canUseVoiceRadio", new object[] { sender, recipient }); - if (should != null) + if (canUse != null) + { return should.Value; + } if (recipientRadio.CanReceive(senderRadio)) { return true; } } - var should2 = GameMain.LuaCs.Hook.Call("changeLocalVoiceRange", sender, recipient); - float range = 1.0f; + float range = GameMain.LuaCs.Hook.Call("changeLocalVoiceRange", sender, recipient) ?? 1.0f; - if (should2 != null) - range = should2.Value; - - - //otherwise do a distance check - return ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange) < range; + if (recipientSpectating) + { + if (recipient.SpectatePos == null) { return true; } + distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / ChatMessage.SpeakRange, 0.0f, 1.0f); + return distanceFactor < 1.0f; + } + else + { + //otherwise do a distance check + float garbleAmount = ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange); + distanceFactor = garbleAmount; + return garbleAmount < range; + } } } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index d1f336222..30ef31928 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.14.0 + 0.20.12.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index d88b556aa..40605ddf4 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -7,18 +7,21 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 82b3e7591..76a6d12de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -102,6 +102,8 @@ namespace Barotrauma private bool inDetectable; + public double InDetectableSetTime; + /// /// Should be reset to false each frame and kept indetectable by e.g. a status effect. /// @@ -115,7 +117,8 @@ namespace Barotrauma { inDetectable = value; if (inDetectable) - { + { + InDetectableSetTime = Timing.TotalTime; NeedsUpdate = true; } } @@ -257,9 +260,14 @@ namespace Barotrauma SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime); } + public bool HasSector() + { + return sectorRad < MathHelper.TwoPi; + } + public bool IsWithinSector(Vector2 worldPosition) { - if (sectorRad >= MathHelper.TwoPi) { return true; } + if (!HasSector()) { return true; } Vector2 diff = worldPosition - WorldPosition; return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 1708c86da..6d0e43c18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -442,6 +442,7 @@ namespace Barotrauma base.Update(deltaTime); UpdateTriggers(deltaTime); Character.ClearInputs(); + Reverse = false; bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) @@ -689,7 +690,7 @@ namespace Barotrauma //if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it //TODO: a way for pets to differentiate hostile and friendly humans? - if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget)) + if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter)) { // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); @@ -804,10 +805,6 @@ namespace Barotrauma Reverse = true; run = true; } - else - { - Reverse = false; - } SteeringManager.SteeringManual(deltaTime, dir * 0.2f); } else @@ -918,7 +915,7 @@ namespace Barotrauma else { // Wander around outside or swimming - steeringManager.SteeringWander(); + steeringManager.SteeringWander(avoidWanderingOutsideLevel: true); if (Character.AnimController.InWater) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); @@ -1086,7 +1083,7 @@ namespace Barotrauma State = AIState.Idle; return; } - else + else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { SelectedAiTarget = owner.AiTarget; } @@ -1490,40 +1487,26 @@ namespace Barotrauma canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); if (canAttack && AttackLimb.attack.AvoidFriendlyFire) { - float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); - bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; - if (SwarmBehavior != null) + canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity)); + bool IsBlocked(Vector2 targetPosition) { - canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c)); - } - else - { - canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c)); - } - if (canAttack) - { - canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range)); - - bool IsBlocked(Vector2 targetPosition) + foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) { - foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + Character hitTarget = null; + if (body.UserData is Character c) { - Character hitTarget = null; - if (body.UserData is Character c) - { - hitTarget = c; - } - else if (body.UserData is Limb limb) - { - hitTarget = limb.character; - } - if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) - { - return true; - } + hitTarget = c; + } + else if (body.UserData is Limb limb) + { + hitTarget = limb.character; + } + if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) + { + return true; } - return false; } + return false; } } } @@ -1853,8 +1836,41 @@ namespace Barotrauma float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); } } - - if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) + if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + { + bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; + bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); + if (fallBack) + { + Reverse = true; + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + else if (advance) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else + { + if (Character.CurrentHull == null && !canAttack) + { + SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + else + { + SteeringManager.Reset(); + FaceTarget(SelectedAiTarget.Entity); + } + } + } + else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1865,20 +1881,22 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else if (AttackLimb.attack.Ranged) - { - // Too close - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); - } + if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } } } + Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; + IDamageable damageTarget = targetEntity as IDamageable; + if (AttackLimb?.attack is Attack { Ranged: true} attack) + { + AimRangedAttack(attack, targetEntity); + } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) + if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); } @@ -1889,6 +1907,22 @@ namespace Barotrauma } } + public void AimRangedAttack(Attack attack, Entity targetEntity) + { + if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } + Character.SetInput(InputType.Aim, false, true); + Limb limb = GetLimbToRotate(attack); + if (limb != null) + { + Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition; + float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + limb.body.SuppressSmoothRotationCalls = false; + float angle = MathUtils.VectorToAngle(toTarget); + limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque); + limb.body.SuppressSmoothRotationCalls = true; + } + } + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) { if (attackingLimb == null) { return false; } @@ -2114,13 +2148,13 @@ namespace Barotrauma } // 10 dmg, 100 health -> 0.1 - private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); + private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); - private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) + private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null) { if (SelectedAiTarget?.Entity == null) { return false; } - if (attackingLimb?.attack == null) { return false; } - ActiveAttack = attackingLimb.attack; + if (AttackLimb?.attack == null) { return false; } + if (damageTarget == null) { return false; } if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -2129,85 +2163,97 @@ namespace Barotrauma { SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); State = AIState.Attack; + return true; } } - IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; - if (damageTarget != null) + ActiveAttack = AttackLimb.attack; + if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0) { - if (Character.Params.CanInteract && Character.Inventory != null) + Limb referenceLimb = GetLimbToRotate(ActiveAttack); + if (referenceLimb != null) { - // Use equipped items (weapons) - Item item = GetEquippedItem(attackingLimb); - if (item != null) + Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition; + float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir); + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); + if (angle > ActiveAttack.RequiredAngleToShoot) { - if (item.RequireAimToUse) - { - if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) - { - // Valid target, but can't shoot -> return true so that it will not be ignored. - return true; - } - } - Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, Character); + return true; } } - //simulate attack input to get the character to attack client-side - Character.SetInput(InputType.Attack, true, true); - if (!ActiveAttack.IsRunning) + } + if (Character.Params.CanInteract && Character.Inventory != null) + { + // Use equipped items (weapons) + Item item = GetEquippedItem(AttackLimb); + if (item != null) { -#if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( - attackingLimb, - damageTarget, - targetLimb, - SimPosition)); -#else - Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); -#endif - } - - if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) - { - if (attackingLimb.attack.CoolDownTimer > 0) + if (item.RequireAimToUse) + { + if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) + { + // Valid target, but can't shoot -> return true so that it will not be ignored. + return true; + } + } + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, Character); + } + } + //simulate attack input to get the character to attack client-side + Character.SetInput(InputType.Attack, true, true); + if (!ActiveAttack.IsRunning) + { +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( + AttackLimb, + damageTarget, + targetLimb, + SimPosition)); +#else + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); +#endif + } + + if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) + { + if (ActiveAttack.CoolDownTimer > 0) + { + SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f)); + // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon + float greed = AIParams.AggressionGreed; + if (damageTarget is not Barotrauma.Character) + { + // Halve the greed for attacking non-characters. + greed /= 2; + } + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; + } + if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) + { + LatchOntoAI.SetAttachTarget(targetCharacter); + } + if (!ActiveAttack.Ranged) + { + if (damageTarget.Health > 0 && attackResult.Damage > 0) { - SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f)); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; - if (!(damageTarget is Character)) + if (damageTarget is not Barotrauma.Character) { // Halve the greed for attacking non-characters. greed /= 2; } selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } - if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) + else { - LatchOntoAI.SetAttachTarget(targetCharacter); - } - if (!attackingLimb.attack.Ranged) - { - if (damageTarget.Health > 0 && attackResult.Damage > 0) - { - // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - float greed = AIParams.AggressionGreed; - if (!(damageTarget is Character)) - { - // Halve the greed for attacking non-characters. - greed /= 2; - } - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; - } - else - { - selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); - return selectedTargetMemory.Priority > 1; - } + selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); + return selectedTargetMemory.Priority > 1; } } - return true; } - return false; + return true; } private float aimTimer; @@ -2299,7 +2345,6 @@ namespace Barotrauma { if (attackVector == null) { - // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); @@ -2319,6 +2364,16 @@ namespace Barotrauma return true; } + private Limb GetLimbToRotate(Attack attack) + { + Limb limb = AttackLimb; + if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length) + { + limb = Character.AnimController.Limbs[attack.RotationLimbIndex]; + } + return limb; + } + #endregion #region Eat @@ -3429,7 +3484,7 @@ namespace Barotrauma private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting); - private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false) + private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false) { if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) { @@ -3437,6 +3492,11 @@ namespace Barotrauma { if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams)) { + if (state == AIState.Attack) + { + // Only applies to new temp target params. Shouldn't affect any existing definitions (handled below). + targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub; + } tempParams.Add(tag, targetParams); } } @@ -3470,7 +3530,7 @@ namespace Barotrauma { isStateChanged = true; SetStateResetTimer(); - ChangeParams(target.SpeciesName, state, priority); + ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman); if (target.IsHuman) { priority = GetTargetParams("human")?.Priority; @@ -3554,7 +3614,10 @@ namespace Barotrauma { // We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible. // When the monsters attack the player sub, they wall hack so that they can be more aggressive. - checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; + // Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack) + checkVisibility = + Character.IsPet && (Character.Submarine != null || target.Entity.Submarine != null) || + target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; } if (dist > 0) { @@ -3755,6 +3818,7 @@ namespace Barotrauma { SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority); State = AIState.Attack; + return false; } } } @@ -3767,7 +3831,7 @@ namespace Barotrauma } else { - SteeringManager.SteeringWander(); + SteeringManager.SteeringWander(avoidWanderingOutsideLevel: Character.CurrentHull == null); if (Character.CurrentHull == null) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); @@ -3826,9 +3890,8 @@ namespace Barotrauma return targetLimb; } - private Character GetOwner(Item item) + private static Character GetOwner(Item item) { - // If the item is held by a character, attack the character instead. var pickable = item.GetComponent(); if (pickable != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 13e861573..006cbfb72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -395,6 +395,10 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); + if (reportProblemsTimer > 0) + { + reportProblemsTimer -= deltaTime; + } if (reactTimer > 0.0f) { reactTimer -= deltaTime; @@ -407,7 +411,6 @@ namespace Barotrauma else { Character.UpdateTeam(); - if (Character.CurrentHull != null) { if (Character.IsOnPlayerTeam) @@ -425,19 +428,15 @@ namespace Barotrauma } } } - if (Character.SpeechImpediment < 100.0f) + if (reportProblemsTimer <= 0.0f) { - reportProblemsTimer -= deltaTime; - if (reportProblemsTimer <= 0.0f) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) - { - ReportProblems(); - } - reportProblemsTimer = reportProblemsInterval; + ReportProblems(); } - UpdateSpeaking(); + reportProblemsTimer = reportProblemsInterval; } + UpdateSpeaking(); UnequipUnnecessaryItems(); reactTimer = GetReactionTime(); } @@ -912,7 +911,7 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; - bool speak = true; + bool speak = Character.SpeechImpediment < 100; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -1063,17 +1062,15 @@ namespace Barotrauma private void UpdateSpeaking() { if (!Character.IsOnPlayerTeam) { return; } - + if (Character.SpeechImpediment >= 100) { return; } if (Character.Oxygen < 20.0f) { Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); } - if (Character.Bleeding > 2.0f) { Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); } - if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) { Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); @@ -1860,6 +1857,7 @@ namespace Barotrauma bool targetAdded = false; DoForEachCrewMember(caller, humanAI => { + if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } var objective = humanAI.ObjectiveManager.GetObjective(); if (objective != null) { @@ -1909,8 +1907,8 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreWater = HasDivingSuit(character); - bool ignoreOxygen = ignoreWater || HasDivingMask(character); + bool ignoreWater = character.IsProtectedFromPressure(); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index f3a2a9c39..4ddfe05c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -77,11 +77,11 @@ namespace Barotrauma { if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } + if (GameMain.GameSession.RoundDuration < 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f && + if (GameMain.GameSession.RoundDuration < 120.0f && speaker?.CurrentHull != null && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index c0c6f9c69..e277590bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -748,6 +748,9 @@ namespace Barotrauma } if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType)) { + //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter + character.ClearInput(InputType.Aim); + character.ClearInput(InputType.Shoot); Weapon.TryInteract(character, forceSelectKey: true); var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); if (character.Inventory.TryPutItem(Weapon, character, slots)) @@ -764,7 +767,7 @@ namespace Barotrauma } return true; - bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); + static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); } private float findHullTimer; @@ -873,7 +876,20 @@ namespace Barotrauma TargetName = Enemy.DisplayName, AlwaysUseEuclideanDistance = false }, - onAbandon: () => Abandon = true); + onAbandon: () => + { + if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + { + // If in the same room with an enemy -> don't try to escape because we'd want to fight it + SteeringManager.Reset(); + RemoveSubObjective(ref followTargetObjective); + } + else + { + // else abandon and fall back to find safety mode + Abandon = true; + } + }); if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 8a5c606b3..c9f531f12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -25,6 +25,8 @@ namespace Barotrauma private readonly Item item; public Item ItemToContain { get; private set; } + public int? TargetSlot; + private AIObjectiveGetItem getItemObjective; private AIObjectiveGoTo goToObjective; @@ -126,7 +128,19 @@ namespace Barotrauma } if (character.CanInteractWith(container.Item, checkLinked: false)) { - if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain))) + static bool CanBePut(Inventory inventory, int? targetSlot, Item itemToContain) + { + if (targetSlot.HasValue) + { + return inventory.CanBePutInSlot(itemToContain, targetSlot.Value); + } + else + { + return inventory.CanBePut(itemToContain); + } + } + + if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain))) { HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax); } @@ -136,7 +150,20 @@ namespace Barotrauma } Inventory originalInventory = ItemToContain.ParentInventory; var slots = originalInventory?.FindIndices(ItemToContain); - if (container.Inventory.TryPutItem(ItemToContain, null)) + + static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) + { + if (targetSlot.HasValue) + { + return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null); + } + else + { + return inventory.TryPutItem(itemToContain, user: null); + } + } + + if (TryPutItem(container.Inventory, TargetSlot, ItemToContain)) { if (MoveWholeStack && slots != null) { @@ -144,7 +171,7 @@ namespace Barotrauma { foreach (Item item in originalInventory.GetItemsAt(slot).ToList()) { - container.Inventory.TryPutItem(item, null); + TryPutItem(container.Inventory, TargetSlot, item); } } } @@ -211,6 +238,15 @@ namespace Barotrauma } } + public bool IsInTargetSlot(Item item) + { + if (container?.Inventory is ItemInventory inventory && TargetSlot is not null) + { + return inventory.IsInSlot(item, (int)TargetSlot); + } + return false; + } + public override void Reset() { base.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 0704b91c6..e525b613c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -23,9 +23,8 @@ namespace Barotrauma protected override float TargetEvaluation() { - if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; } - int totalEnemies = Targets.Count(); - if (totalEnemies == 0) { return 0; } + if (Targets.None()) { return 0; } + if (!character.IsOnPlayerTeam) { return 100; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index c2c9f8ee5..0c3ae42a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -103,13 +103,21 @@ namespace Barotrauma character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f); } } - return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + var container = targetItem.GetComponent(); + var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, ConditionLevel = MIN_OXYGEN, RemoveExistingWhenNecessary = true }; + if (container.HasSubContainers) + { + objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + } + // Only remove the oxygen source being replaced + objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i); + return objective; }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index d8607e729..684713e02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -262,7 +262,7 @@ namespace Barotrauma { Character followTarget = Target as Character; bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)); + bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); if (Mimic) { if (HumanAIController.HasDivingSuit(followTarget)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 30d687a2a..cf8cf19ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -413,28 +413,10 @@ namespace Barotrauma } } } - + private void ApplyTreatment(Affliction affliction, Item item) { - var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction); - bool remove = false; - foreach (ItemComponent ic in item.Components) - { - if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; } -#if CLIENT - ic.PlaySound(ActionType.OnUse, character); -#endif - ic.WasUsed = true; - ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character); - if (ic.DeleteOnUse) - { - remove = true; - } - } - if (remove) - { - Entity.Spawner?.AddItemToRemoveQueue(item); - } + item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); } protected override bool CheckObjectiveSpecific() @@ -502,10 +484,12 @@ namespace Barotrauma public static IEnumerable GetTreatableAfflictions(Character character) { - foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + var allAfflictions = character.CharacterHealth.GetAllAfflictions(); + foreach (Affliction affliction in allAfflictions) { if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; } + if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 1105eab19..4f64dac4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using static Barotrauma.CharacterParams; namespace Barotrauma { @@ -44,7 +45,7 @@ namespace Barotrauma public float PlayTimer { get; set; } private float? unstunY { get; set; } - public EnemyAIController AiController { get; private set; } = null; + public EnemyAIController AIController { get; private set; } = null; public Character Owner { get; set; } @@ -134,8 +135,8 @@ namespace Barotrauma aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); - Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); + Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); break; } } @@ -160,8 +161,8 @@ namespace Barotrauma public PetBehavior(XElement element, EnemyAIController aiController) { - AiController = aiController; - AiController.Character.CanBeDragged = true; + AIController = aiController; + AIController.Character.CanBeDragged = true; MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); @@ -218,7 +219,7 @@ namespace Barotrauma bool success = OnEat(item.GetTags()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier); } return success; } @@ -229,7 +230,7 @@ namespace Barotrauma bool success = OnEat("dead".ToIdentifier()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName); } return success; } @@ -252,7 +253,7 @@ namespace Barotrauma Hunger += foods[i].Hunger; Happiness += foods[i].Happiness; #if CLIENT - AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); + AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); #endif return true; } @@ -265,20 +266,20 @@ namespace Barotrauma if (PlayTimer > 0.0f) { return; } if (Owner == null) { Owner = player; } PlayTimer = 5.0f; - AiController.Character.IsRagdolled = true; + AIController.Character.IsRagdolled = true; Happiness += 10.0f; - AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); - unstunY = AiController.Character.SimPosition.Y; + AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); + unstunY = AIController.Character.SimPosition.Y; #if CLIENT - AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); + AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); #endif } public string GetTagName() { - if (AiController.Character.Inventory != null) + if (AIController.Character.Inventory != null) { - foreach (Item item in AiController.Character.Inventory.AllItems) + foreach (Item item in AIController.Character.Inventory.AllItems) { var tag = item.GetComponent(); if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName)) @@ -293,7 +294,7 @@ namespace Barotrauma public void Update(float deltaTime) { - var character = AiController.Character; + var character = AIController.Character; if (character?.Removed ?? true || character.IsDead) { return; } if (unstunY.HasValue) @@ -332,16 +333,27 @@ namespace Barotrauma Food food = foods[i]; if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y) { - if (food.TargetParams == null && - AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams)) + if (food.TargetParams == null) { - targetParams.IgnoreContained = food.IgnoreContained; - food.TargetParams = targetParams; + if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target)) + { + food.TargetParams = target; + } + else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams)) + { + food.TargetParams = targetParams; + } + if (food.TargetParams != null) + { + food.TargetParams.State = AIState.Eat; + food.TargetParams.Priority = food.Priority; + food.TargetParams.IgnoreContained = food.IgnoreContained; + } } } else if (food.TargetParams != null) { - AiController.AIParams.RemoveTarget(food.TargetParams); + AIController.AIParams.RemoveTarget(food.TargetParams); food.TargetParams = null; } } @@ -423,7 +435,7 @@ namespace Barotrauma spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } - var pet = Character.Create(speciesName, spawnPos, seed); + var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false); var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior; if (petBehavior != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 95ff37b9c..b1a83f852 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -116,10 +116,10 @@ namespace Barotrauma } // accept only the highest priority order - if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) + if (CurrentOrder == null || OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) { #if DEBUG - ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); + ShipCommandManager.ShipCommandLog($"{this} is no longer the top priority of {OrderedCharacter}, considering the issue unattended."); #endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index a112758f1..b4a18d756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -356,7 +356,7 @@ namespace Barotrauma ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); } - foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret"))) + foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint"))) { var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent()); ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index e4e900990..f415c5b96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -43,9 +43,9 @@ namespace Barotrauma steering += DoSteeringSeek(targetSimPos, weight); } - public void SteeringWander(float weight = 1) + public void SteeringWander(float weight = 1, bool avoidWanderingOutsideLevel = false) { - steering += DoSteeringWander(weight); + steering += DoSteeringWander(weight, avoidWanderingOutsideLevel); } public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1) @@ -119,7 +119,7 @@ namespace Barotrauma //return newSteering; } - protected virtual Vector2 DoSteeringWander(float weight) + protected virtual Vector2 DoSteeringWander(float weight, bool avoidWanderingOutsideLevel) { Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Vector2.UnitY : host.Steering; circleCenter = Vector2.Normalize(circleCenter) * CircleDistance; @@ -127,19 +127,35 @@ namespace Barotrauma Vector2 displacement = new Vector2( (float)Math.Cos(wanderAngle), (float)Math.Sin(wanderAngle)); - displacement = displacement * CircleRadius; + displacement *= CircleRadius; float angleChange = 1.5f; wanderAngle += Rand.Range(0.0f, 1.0f) * angleChange - angleChange * 0.5f; Vector2 newSteering = circleCenter + displacement; + if (avoidWanderingOutsideLevel && Level.Loaded != null) + { + float margin = 5000.0f; + if (host.WorldPosition.X < -margin) + { + // Too far left + newSteering.X += (-margin - host.WorldPosition.X) * weight / margin; + } + else if (host.WorldPosition.X > Level.Loaded.Size.X - margin) + { + // Too far right + newSteering.X -= (host.WorldPosition.X - (Level.Loaded.Size.X - margin)) * weight / margin; + } + } + float steeringSpeed = (newSteering + host.Steering).Length(); if (steeringSpeed > weight) { newSteering = Vector2.Normalize(newSteering) * weight; } + return newSteering; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 7981478e6..48ad56141 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -11,8 +11,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) + public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null, bool spawnInitialItems = true) + : base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll, spawnInitialItems) { InitProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 928dd8e74..a2e4e7a3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -87,7 +87,7 @@ namespace Barotrauma } public bool CanWalk => RagdollParams.CanWalk; - public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir); + public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false }; // TODO: define death anim duration in XML protected float deathAnimTimer, deathAnimDuration = 5.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 8aaa57eb7..9d91ba35a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -610,15 +610,18 @@ namespace Barotrauma torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!head.Disabled) { - float headAngle = HeadAngle.Value; - if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } - head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); - } - else - { - RotateHead(head); + if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + { + float headAngle = HeadAngle.Value; + if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } + head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); + } + else + { + RotateHead(head); + } } if (!onGround) @@ -1368,7 +1371,7 @@ namespace Barotrauma Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); - + Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition; targetMovement = new Vector2(diff.X, 0.0f); TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left; @@ -1383,15 +1386,25 @@ namespace Barotrauma float prevVitality = target.Vitality; bool wasCritical = prevVitality < 0.0f; - + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code { target.Oxygen += deltaTime * 0.5f; //Stabilize them } - bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR); - + float cprBoost = character.GetStatValue(StatTypes.CPRBoost); + int skill = (int)character.GetSkillLevel("medical"); + + 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); + } + } + //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) if (cprAnimTimer > 15.0f && targetHead != null && head != null) { @@ -1402,23 +1415,15 @@ namespace Barotrauma torso.PullJointEnabled = true; //Serverside code - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + if (GameMain.NetworkMember is not { IsClient: true }) { if (target.Oxygen < -10.0f) { - if (powerfulCPR) - { - //prevent the patient from suffocating no matter how fast their oxygen level is dropping - target.Oxygen = Math.Max(target.Oxygen, -10.0f); - } - else - { - //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 - } + //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 } } } @@ -1453,7 +1458,7 @@ namespace Barotrauma reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); - if (powerfulCPR) { reviveChance *= 2.0f; } + reviveChance *= 1f + cprBoost; if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) { @@ -1839,8 +1844,6 @@ namespace Barotrauma { heldItem.FlipX(relativeToSub: false); } - // TODO: was this added by a mistake? - //heldItem.FlipX(relativeToSub: false); } foreach (Limb limb in Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index dd3e91cf1..5bcfb552e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -881,7 +881,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered) { continue; } + if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; } limb.Dir = Dir; limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MirrorPullJoint(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index d5d02d2d4..16657084f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -189,6 +190,15 @@ namespace Barotrauma [Serialize(20f, IsPropertySaveable.Yes)] public float RequiredAngle { get; set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable] + public float RequiredAngleToShoot { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable] + public float AimRotationTorque { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable] + public int RotationLimbIndex { get; set; } + /// /// Legacy support. Use Afflictions. /// @@ -196,7 +206,7 @@ namespace Barotrauma public float Stun { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] - public bool OnlyHumans { get; private set; } + public bool OnlyHumans { get; set; } [Serialize("", IsPropertySaveable.Yes), Editable] public string ApplyForceOnLimbs @@ -312,7 +322,7 @@ namespace Barotrauma List multipliedAfflictions = new List(); foreach (Affliction affliction in Afflictions.Keys) { - multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability)); + multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction)); } return multipliedAfflictions; } @@ -521,7 +531,7 @@ namespace Barotrauma effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effectType, deltaTime, targetEntity, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) @@ -529,6 +539,12 @@ namespace Barotrauma effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); } } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { + targets.Clear(); + targets.AddRange(attacker.Inventory.AllItems); + effect.Apply(effectType, deltaTime, attacker, targets); + } } return attackResult; @@ -554,7 +570,15 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); - var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration); + float penetration = Penetration; + + float? penetrationValue = SourceItem?.GetComponent()?.Penetration; + if (penetrationValue.HasValue) + { + penetration += penetrationValue.Value; + } + + var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) @@ -584,13 +608,19 @@ namespace Barotrauma effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effectType, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition); } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { + targets.Clear(); + targets.AddRange(attacker.Inventory.AllItems); + effect.Apply(effectType, deltaTime, attacker, targets); + } } return attackResult; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index f4ec02df2..8f4727b5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -25,6 +25,8 @@ namespace Barotrauma FriendlyNPC = 3 } + public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier); + partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync { public readonly static List CharacterList = new List(); @@ -132,6 +134,7 @@ namespace Barotrauma public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active); public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; public bool IsEscorted { get; set; } + public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty; public readonly Dictionary Properties; public Dictionary SerializableProperties @@ -349,7 +352,7 @@ namespace Barotrauma private readonly Dictionary itemSelectedDurations = new Dictionary(); private double itemSelectedTime; - public float InvisibleTimer; + public float InvisibleTimer { get; set; } public readonly CharacterPrefab Prefab; @@ -529,7 +532,16 @@ namespace Barotrauma private Color speechBubbleColor; private float speechBubbleTimer; - public bool ResetInteract; + /// + /// Prevents the character from interacting with items or characters + /// + public bool DisableInteract { get; set; } + + /// + /// Prevents the character from highlighting items or characters with the cursor, + /// meaning it can't interact with anything but the things it has currently selected/equipped + /// + public bool DisableFocusingOnEntities { get; set; } //text displayed when the character is highlighted if custom interact is set public LocalizedString CustomInteractHUDText { get; private set; } @@ -614,7 +626,9 @@ namespace Barotrauma CharacterHealth.SetHealthBarVisibility(value == null); #endif bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; - if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) + CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value)); + + if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet }) { #if SERVER if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) @@ -796,6 +810,7 @@ namespace Barotrauma public float MaxHealth => MaxVitality; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; + public float EmpVulnerability => Params.Health.EmpVulnerability; public float Bloodloss { @@ -843,6 +858,12 @@ namespace Barotrauma set; } + public bool IgnoreMeleeWeapons + { + get; + set; + } + /// /// Current speed of the character's collider. Can be used by status effects to check if the character is moving. /// @@ -893,6 +914,10 @@ namespace Barotrauma { itemSelectedTime = Timing.TotalTime; } + if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null) + { + prevSelectedItem.OnDeselect(this); + } } } /// @@ -1002,7 +1027,7 @@ namespace Barotrauma } } - public bool InWater => AnimController?.InWater ?? false; + public bool InWater => AnimController is AnimController { InWater: true }; public bool GodMode = false; @@ -1056,6 +1081,8 @@ namespace Barotrauma } } + public HashSet MarkedAsLooted = new(); + public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); @@ -1073,9 +1100,9 @@ namespace Barotrauma /// Is the character controlled by a remote player. /// Is the character controlled by AI. /// Ragdoll configuration file. If null, will select the default. - public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null) + public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null, bool spawnInitialItems = true) { - return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, true, ragdoll); + return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent: true, ragdoll, spawnInitialItems); } /// @@ -1090,16 +1117,16 @@ namespace Barotrauma /// Is the character controlled by AI. /// Should clients receive a network event about the creation of this character? /// Ragdoll configuration file. If null, will select the default. - public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true) + public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true) { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { speciesName = Path.GetFileNameWithoutExtension(speciesName); } - return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound); + return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound, spawnInitialItems); } - public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true) + public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true) { var prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab == null) @@ -1116,29 +1143,29 @@ namespace Barotrauma return null; } - return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll); + return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, spawnInitialItems); } - public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true) { Character newCharacter = null; if (prefab.Identifier != CharacterPrefab.HumanSpeciesName) { - var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else { - newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); } #if SERVER @@ -1158,7 +1185,7 @@ namespace Barotrauma wallet = new Wallet(Option.Some(this)); } - protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) + protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true) : this(null, id) { this.Seed = seed; @@ -1268,7 +1295,8 @@ namespace Barotrauma { Inventory = new CharacterInventory( inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random), - this); + this, + spawnInitialItems); } if (healthElements.Count == 0) { @@ -1373,7 +1401,28 @@ namespace Barotrauma tags.RemoveWhere(t => t.StartsWith("variant")); tags.Add($"variant{headId.Value}".ToIdentifier()); } + var oldHeadInfo = Info.Head; Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + if (hairIndex == -1) + { + Info.Head.HairIndex = oldHeadInfo.HairIndex; + } + if (beardIndex == -1) + { + Info.Head.BeardIndex = oldHeadInfo.BeardIndex; + } + if (moustacheIndex == -1) + { + Info.Head.MoustacheIndex = oldHeadInfo.MoustacheIndex; + } + if (faceAttachmentIndex == -1) + { + Info.Head.FaceAttachmentIndex = oldHeadInfo.FaceAttachmentIndex; + } + Info.Head.SkinColor = oldHeadInfo.SkinColor; + Info.Head.HairColor = oldHeadInfo.HairColor; + Info.Head.FacialHairColor = oldHeadInfo.FacialHairColor; + Info.CheckColors(); #if CLIENT head.RecreateSprites(); #endif @@ -1581,19 +1630,37 @@ namespace Barotrauma } if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()])); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item)); } } } public float GetSkillLevel(string skillIdentifier) => GetSkillLevel(skillIdentifier.ToIdentifier()); - + + private static readonly ImmutableDictionary overrideStatTypes = new Dictionary + { + { new("helm"), StatTypes.HelmSkillOverride }, + { new("medical"), StatTypes.MedicalSkillOverride }, + { new("weapons"), StatTypes.WeaponsSkillOverride }, + { new("electrical"), StatTypes.ElectricalSkillOverride }, + { new("mechanical"), StatTypes.MechanicalSkillOverride } + }.ToImmutableDictionary(); + public float GetSkillLevel(Identifier skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType)) + { + float skillOverride = GetStatValue(statType); + if (skillOverride > skillLevel) + { + skillLevel = skillOverride; + } + } + // apply multipliers first so that multipliers only affect base skill value foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) { @@ -1624,6 +1691,7 @@ namespace Barotrauma skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); + return skillLevel; } @@ -1955,6 +2023,15 @@ namespace Barotrauma } } #endif + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim)) + { + if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi) + { + enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity); + } + } + if (attackCoolDown > 0.0f) { attackCoolDown -= deltaTime; @@ -1965,7 +2042,7 @@ namespace Barotrauma { if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false) { - currentAttackTarget = default(AttackTargetData); + currentAttackTarget = default; } currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); } @@ -2060,35 +2137,50 @@ namespace Barotrauma } } - bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; - if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) + if (Inventory != null) { - foreach (Item item in HeldItems) + bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; + if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) { - if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse) + foreach (Item item in HeldItems) { - item.SecondaryUse(deltaTime, this); + tryUseItem(item, deltaTime); } - if (IsKeyDown(InputType.Use) && !item.IsShootable) + foreach (Item item in Inventory.AllItems) { - if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) + if (item.GetComponent() is { AllowUseWhenWorn: true } && HasEquippedItem(item)) { - item.Use(deltaTime, this); + tryUseItem(item, deltaTime); } } - if (IsKeyDown(InputType.Shoot) && item.IsShootable) + } + } + + void tryUseItem(Item item, float deltaTime) + { + if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse) + { + item.SecondaryUse(deltaTime, this); + } + if (IsKeyDown(InputType.Use) && !item.IsShootable) + { + if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) { - if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - item.Use(deltaTime, this); - } + item.Use(deltaTime, this); + } + } + if (IsKeyDown(InputType.Shoot) && item.IsShootable) + { + if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) + { + item.Use(deltaTime, this); + } #if CLIENT - else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) - { - HintManager.OnShootWithoutAiming(this, item); - } -#endif + else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) + { + HintManager.OnShootWithoutAiming(this, item); } +#endif } } @@ -2623,9 +2715,9 @@ namespace Barotrauma return; } - if (ResetInteract) + if (DisableInteract) { - ResetInteract = false; + DisableInteract = false; return; } @@ -2644,27 +2736,33 @@ namespace Barotrauma #if CLIENT if (isLocalPlayer) { - if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this)) + if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities) { - if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)) + if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) { - FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; - if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } - float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); - if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) + if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld) { - //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes - aimAssist = 0.0f; - } + FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; + if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } + float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); + if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) + { + //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes + aimAssist = 0.0f; + } - var item = FindItemAtPosition(mouseSimPos, aimAssist); - - focusedItem = CanInteract ? item : null; - if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) - { - FocusedCharacter = null; + focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null; + if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) + { + FocusedCharacter = null; + } + findFocusedTimer = 0.05f; + } + else + { + if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; } + if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; } } - findFocusedTimer = 0.05f; } } else @@ -2673,6 +2771,7 @@ namespace Barotrauma focusedItem = null; } findFocusedTimer -= deltaTime; + DisableFocusingOnEntities = false; } #endif var head = AnimController.GetLimb(LimbType.Head); @@ -2907,6 +3006,15 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime, cam); + if (InvisibleTimer > 0.0f) + { + if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + { + InvisibleTimer = Math.Min(InvisibleTimer, 1.0f); + } + InvisibleTimer -= deltaTime; + } + KnockbackCooldownTimer -= deltaTime; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; } @@ -2946,6 +3054,7 @@ namespace Barotrauma } HideFace = false; + IgnoreMeleeWeapons = false; UpdateSightRange(deltaTime); UpdateSoundRange(deltaTime); @@ -3023,7 +3132,8 @@ namespace Barotrauma ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(ActionType.OnActive, deltaTime); - if (aiTarget != null) + //wait 0.1 seconds so status effects that continuously set InDetectable to true can keep the character InDetectable + if (aiTarget != null && Timing.TotalTime > aiTarget.InDetectableSetTime + 0.1f) { aiTarget.InDetectable = false; } @@ -3809,7 +3919,7 @@ namespace Barotrauma return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } #if DEBUG @@ -3819,7 +3929,7 @@ namespace Barotrauma return; } #endif - if (damage < targetLimb.Params.MinSeveranceDamage) { return; } + if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; } if (!IsDead) { if (!allowBeheading && targetLimb.type == LimbType.Head) { return; } @@ -3837,7 +3947,7 @@ namespace Barotrauma var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB; if (referenceLimb != targetLimb) { continue; } float probability = severLimbsProbability; - if (!IsDead) + if (!IsDead && !ignoreSeveranceProbabilityModifier) { probability *= joint.Params.SeveranceProbabilityModifier; } @@ -4051,7 +4161,13 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (Screen.Selected != GameMain.GameScreen) { return; } - if (newStun > 0 && Params.Health.StunImmunity) { return; } + if (newStun > 0 && Params.Health.StunImmunity) + { + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + { + return; + } + } if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; } if (Math.Sign(newStun) != Math.Sign(Stun)) { @@ -4096,7 +4212,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, this, targets); } else if (statusEffect.targetLimbs != null) @@ -4165,11 +4281,14 @@ namespace Barotrauma if (!isNetworkMessage) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } } CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength)); - if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); } + if (GameMain.NetworkMember is not { IsClient: true } || isNetworkMessage) + { + Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); + } if (IsDead) { BreakJoints(); @@ -4730,6 +4849,8 @@ namespace Barotrauma public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; + public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier; + public bool IsProtectedFromPressure() { return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); @@ -4810,6 +4931,32 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + private readonly HashSet sameRoomHulls = new(); + + /// + /// Check if the character is in the same room + /// Room and hull differ in that a room can consist of multiple linked hulls + /// + public bool IsInSameRoomAs(Character character) + { + if (character == this) { return true; } + + if (character.CurrentHull is null || CurrentHull is null) + { + // Outside doesn't count as a room + return false; + } + + if (character.Submarine != Submarine) { return false; } + if (character.CurrentHull == CurrentHull) { return true; } + + sameRoomHulls.Clear(); + CurrentHull.GetLinkedEntities(sameRoomHulls); + sameRoomHulls.Add(CurrentHull); + + return sameRoomHulls.Contains(character.CurrentHull); + } + public bool HasUnlockedAllTalents() { if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) @@ -4818,7 +4965,7 @@ namespace Barotrauma { foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) { - if (talentOption.TalentIdentifiers.None(t => HasTalent(t))) + if (!talentOption.HasMaxTalents(info.UnlockedTalents)) { return false; } @@ -4863,6 +5010,19 @@ namespace Barotrauma return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); } + public bool HasStoreAccessForItem(ItemPrefab prefab) + { + foreach (CharacterTalent talent in characterTalents) + { + foreach (Identifier unlockedItem in talent.UnlockedStoreItems) + { + if (prefab.Tags.Contains(unlockedItem)) { return true; } + } + } + + return false; + } + /// /// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains. /// @@ -4921,7 +5081,7 @@ namespace Barotrauma /// private readonly Dictionary wearableStatValues = new Dictionary(); - public float GetStatValue(StatTypes statType) + public float GetStatValue(StatTypes statType, bool includeSaved = true) { if (!IsHuman) { return 0f; } @@ -4934,7 +5094,7 @@ namespace Barotrauma { statValue += CharacterHealth.GetStatValue(statType); } - if (Info != null) + if (Info != null && includeSaved) { // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change statValue += Info.GetSavedStatValue(statType); @@ -5019,25 +5179,48 @@ namespace Barotrauma return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } - private readonly Dictionary abilityResistances = new Dictionary(); - + private readonly Dictionary abilityResistances = new(); + public float GetAbilityResistance(AfflictionPrefab affliction) { - return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f; + float resistance = 0f; + bool hadResistance = false; + + foreach (var (key, value) in abilityResistances) + { + if (key.ResistanceIdentifier == affliction.AfflictionType || + key.ResistanceIdentifier == affliction.Identifier) + { + resistance += value; + hadResistance = true; + } + } + + return hadResistance ? resistance : 1f; } - public void ChangeAbilityResistance(Identifier resistanceId, float value) + public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value) { - if (abilityResistances.ContainsKey(resistanceId)) + if (!MathUtils.IsValid(value)) { - abilityResistances[resistanceId] *= value; +#if DEBUG + DebugConsole.ThrowError($"Attempted to set ability resistance to an invalid value ({value})\n" + Environment.StackTrace.CleanupStackTrace()); +#endif + return; + } + + if (abilityResistances.ContainsKey(identifier)) + { + abilityResistances[identifier] *= value; } else { - abilityResistances.Add(resistanceId, value); + abilityResistances.Add(identifier, value); } } + public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier); + /// /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs /// @@ -5075,6 +5258,16 @@ namespace Barotrauma } } + internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter + { + public Character Character { get; set; } + + public AbilityCharacterLoot(Character character) + { + Character = character; + } + } + class AbilityCharacterKill : AbilityObject, IAbilityCharacter { public AbilityCharacterKill(Character character, Character killer) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterHUD.cs new file mode 100644 index 000000000..9970fa388 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterHUD.cs @@ -0,0 +1,12 @@ +namespace Barotrauma; + +partial class CharacterHUD +{ + static partial void RecreateHudTextsIfControllingProjSpecific(Character character); + + static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items); + + public static void RecreateHudTextsIfControlling(Character character) => RecreateHudTextsIfControllingProjSpecific(character); + + public static void RecreateHudTextsIfFocused(params Item[] items) => RecreateHudTextsIfFocusedProjSpecific(items); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 6876a6a22..e65fabba5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -63,27 +63,34 @@ namespace Barotrauma public readonly CharacterInfo CharacterInfo; public readonly HeadPreset Preset; - private int hairIndex; + public int HairIndex { get; set; } - public int HairIndex + private int? hairWithHatIndex; + + public void SetHairWithHatIndex() { - get => hairIndex; - set + if (CharacterInfo.Hairs is null) { - hairIndex = value; - if (CharacterInfo.Hairs is null) + if (HairIndex == -1) { - HairWithHatIndex = value; - return; +#if DEBUG + DebugConsole.ThrowError("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!"); +#else + DebugConsole.AddWarning("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!"); +#endif } - HairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", hairIndex) ?? -1; - if (HairWithHatIndex < 0 || HairWithHatIndex >= CharacterInfo.Hairs.Count) + hairWithHatIndex = HairIndex; + } + else + { + hairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", HairIndex) ?? -1; + if (hairWithHatIndex < 0 || hairWithHatIndex >= CharacterInfo.Hairs.Count) { - HairWithHatIndex = hairIndex; + hairWithHatIndex = HairIndex; } } } - public int HairWithHatIndex { get; private set; } + public int BeardIndex; public int MoustacheIndex; public int FaceAttachmentIndex; @@ -99,26 +106,29 @@ namespace Barotrauma get { if (CharacterInfo.Hairs == null) { return null; } - if (hairIndex >= CharacterInfo.Hairs.Count) + if (HairIndex >= CharacterInfo.Hairs.Count) { - DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})"); + DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})"); } - return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex); + return CharacterInfo.Hairs.ElementAtOrDefault(HairIndex); } } public ContentXElement HairWithHatElement { get { - if (CharacterInfo.Hairs == null) { return null; } - if (HairWithHatIndex >= CharacterInfo.Hairs.Count) + if (hairWithHatIndex == null) { - DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})"); + SetHairWithHatIndex(); } - return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex); + if (CharacterInfo.Hairs == null) { return null; } + if (hairWithHatIndex >= CharacterInfo.Hairs.Count) + { + DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})"); + } + return CharacterInfo.Hairs.ElementAtOrDefault(hairWithHatIndex.Value); } } - public ContentXElement BeardElement { get @@ -711,7 +721,7 @@ namespace Barotrauma private bool IsColorValid(in Color clr) => clr.R != 0 || clr.G != 0 || clr.B != 0; - private void CheckColors() + public void CheckColors() { if (!IsColorValid(Head.HairColor)) { @@ -735,9 +745,7 @@ namespace Barotrauma Name = infoElement.GetAttributeString("name", ""); OriginalName = infoElement.GetAttributeString("originalname", null); Salary = infoElement.GetAttributeInt("salary", 1000); - ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); - UnlockedTalents = new HashSet(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty())); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); LoadTagsBackwardsCompatibility(infoElement, tags); @@ -813,18 +821,22 @@ namespace Barotrauma infoElement.GetAttributeIdentifier("npcid", Identifier.Empty)); MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); + UnlockedTalents = new HashSet(); foreach (var subElement in infoElement.Elements()) { bool jobCreated = false; - if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated) + + Identifier elementName = subElement.Name.ToIdentifier(); + + if (elementName == "job" && !jobCreated) { Job = new Job(subElement); jobCreated = true; // there used to be a break here, but it had to be removed to make room for statvalues // using the jobCreated boolean to make sure that only the first job found is created } - else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase)) + else if (elementName == "savedstatvalues") { foreach (XElement savedStat in subElement.Elements()) { @@ -838,8 +850,8 @@ namespace Barotrauma float value = savedStat.GetAttributeFloat("statvalue", 0f); if (value == 0f) { continue; } - string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant(); - if (string.IsNullOrEmpty(statIdentifier)) + Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty); + if (statIdentifier.IsEmpty) { DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!"); return; @@ -849,6 +861,20 @@ namespace Barotrauma ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); } } + else if (elementName == "talents") + { + Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe + + foreach (XElement talentElement in subElement.Elements()) + { + if (talentElement.Name.ToIdentifier() != "talent") { continue; } + + Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (talentIdentifier == Identifier.Empty) { continue; } + + UnlockedTalents.Add(talentIdentifier); + } + } } LoadHeadAttachments(); } @@ -1149,13 +1175,17 @@ namespace Barotrauma increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed); + increase = GetSkillSpecificGain(increase, skillIdentifier); + float prevLevel = Job.GetSkillLevel(skillIdentifier); Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); float newLevel = Job.GetSkillLevel(skillIdentifier); if ((int)newLevel > (int)prevLevel) - { + { + float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain); + Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); // assume we are getting at least 1 point in skill, since this logic only runs in such cases float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f); var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility); @@ -1169,6 +1199,25 @@ namespace Barotrauma OnSkillChanged(skillIdentifier, prevLevel, newLevel); } + private static readonly ImmutableDictionary skillGainStatValues = new Dictionary + { + { new("helm"), StatTypes.HelmSkillGainSpeed }, + { new("medical"), StatTypes.WeaponsSkillGainSpeed }, + { new("weapons"), StatTypes.MedicalSkillGainSpeed }, + { new("electrical"), StatTypes.ElectricalSkillGainSpeed }, + { new("mechanical"), StatTypes.MechanicalSkillGainSpeed } + }.ToImmutableDictionary(); + + private float GetSkillSpecificGain(float increase, Identifier skillIdentifier) + { + if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType)) + { + increase *= 1f + Character.GetStatValue(statType); + } + + return increase; + } + public void SetSkillLevel(Identifier skillIdentifier, float level) { if (Job == null) { return; } @@ -1189,15 +1238,11 @@ namespace Barotrauma partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel); - public void GiveExperience(int amount, bool isMissionExperience = false) + public void GiveExperience(int amount) { int prevAmount = ExperiencePoints; var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); - if (isMissionExperience) - { - Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); - } experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0; amount = (int)(amount * experienceGainMultiplier.Value); @@ -1247,6 +1292,18 @@ namespace Barotrauma return experienceRequired + ExperienceRequiredPerLevel(level); } + public int GetExperienceRequiredForLevel(int level) + { + int currentLevel = GetCurrentLevel(out int experienceRequired); + if (currentLevel >= level) { return 0; } + int required = experienceRequired; + for (int i = currentLevel + 1; i <= level; i++) + { + required += ExperienceRequiredPerLevel(i); + } + return required; + } + public int GetCurrentLevel() { return GetCurrentLevel(out _); @@ -1264,7 +1321,7 @@ namespace Barotrauma return level; } - private int ExperienceRequiredPerLevel(int level) + private static int ExperienceRequiredPerLevel(int level) { return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level; } @@ -1314,7 +1371,6 @@ namespace Barotrauma new XAttribute("tags", string.Join(",", Head.Preset.TagSet)), new XAttribute("salary", Salary), new XAttribute("experiencepoints", ExperiencePoints), - new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), new XAttribute("additionaltalentpoints", AdditionalTalentPoints), new XAttribute("hairindex", Head.HairIndex), new XAttribute("beardindex", Head.BeardIndex), @@ -1363,7 +1419,16 @@ namespace Barotrauma } } + XElement talentElement = new XElement("Talents"); + talentElement.Add(new XAttribute("version", GameMain.Version.ToString())); + + foreach (Identifier talentIdentifier in UnlockedTalents) + { + talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier))); + } + charElement.Add(savedStatElement); + charElement.Add(talentElement); parentElement?.Add(charElement); return charElement; } @@ -1717,20 +1782,33 @@ namespace Barotrauma } } - public void ResetSavedStatValue(string statIdentifier) + public void ResetSavedStatValue(Identifier statIdentifier) { foreach (StatTypes statType in SavedStatValues.Keys) { bool changed = false; foreach (SavedStatValue savedStatValue in SavedStatValues[statType]) { - if (savedStatValue.StatIdentifier != statIdentifier) { continue; } + if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; } + if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; } savedStatValue.StatValue = 0.0f; changed = true; } if (changed) { OnPermanentStatChanged(statType); } } + + static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier) + { + if (statIdentifier == identifier) { return true; } + + if (identifier.IndexOf('*') is var index and > -1) + { + return statIdentifier.StartsWith(identifier[0..index]); + } + + return false; + } } public float GetSavedStatValue(StatTypes statType) @@ -1748,7 +1826,7 @@ namespace Barotrauma { if (SavedStatValues.TryGetValue(statType, out var statValues)) { - return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue); + return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue); } else { @@ -1756,7 +1834,7 @@ namespace Barotrauma } } - public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) + public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) { if (!SavedStatValues.ContainsKey(statType)) { @@ -1779,13 +1857,13 @@ namespace Barotrauma } } - public class SavedStatValue + internal sealed class SavedStatValue { - public string StatIdentifier { get; set; } + public Identifier StatIdentifier { get; set; } public float StatValue { get; set; } public bool RemoveOnDeath { get; set; } - public SavedStatValue(string statIdentifier, float value, bool removeOnDeath) + public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath) { StatValue = value; RemoveOnDeath = removeOnDeath; @@ -1793,7 +1871,7 @@ namespace Barotrauma } } - class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter + internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter { public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 8d4b7951b..6b5088913 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -27,6 +27,14 @@ namespace Barotrauma get { return _strength; } set { + if (!MathUtils.IsValid(value)) + { +#if DEBUG + DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({value})\n" + Environment.StackTrace.CleanupStackTrace()); +#endif + return; + } + if (_nonClampedStrength < 0 && value > 0) { _nonClampedStrength = value; @@ -53,6 +61,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable] public bool DivideByLimbCount { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable] + public bool MultiplyByMaxVitality { get; private set; } + public float DamagePerSecond; public float DamagePerSecondTimer; public float PreviousVitalityDecrease; @@ -71,6 +82,13 @@ namespace Barotrauma /// public Character Source; + private readonly static LocalizedString[] strengthTexts = new LocalizedString[] + { + TextManager.Get("AfflictionStrengthLow"), + TextManager.Get("AfflictionStrengthMedium"), + TextManager.Get("AfflictionStrengthHigh") + }; + public Affliction(AfflictionPrefab prefab, float strength) { #if CLIENT @@ -89,6 +107,16 @@ namespace Barotrauma } } + /// + /// Copy properties here instead of using SerializableProperties (with reflection). + /// + public void CopyProperties(Affliction source) + { + Probability = source.Probability; + DivideByLimbCount = source.DivideByLimbCount; + MultiplyByMaxVitality = source.MultiplyByMaxVitality; + } + public void Serialize(XElement element) { SerializableProperty.SerializeProperties(this, element); @@ -99,15 +127,26 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public Affliction CreateMultiplied(float multiplier, float probability) + public Affliction CreateMultiplied(float multiplier, Affliction affliction) { var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); - instance.Probability = probability; + instance.CopyProperties(affliction); return instance; } public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})"; + public LocalizedString GetStrengthText() + { + return GetStrengthText(Strength, Prefab.MaxStrength); + } + + public static LocalizedString GetStrengthText(float strength, float maxStrength) + { + return strengthTexts[ + MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + } + public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); public float GetVitalityDecrease(CharacterHealth characterHealth) @@ -424,15 +463,15 @@ namespace Barotrauma { statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb); } - if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + if (characterHealth?.Character?.AnimController?.Limbs != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs); + statusEffect.Apply(type, deltaTime, characterHealth.Character, targets: characterHealth.Character.AnimController.Limbs); } if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets)); + statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets); statusEffect.Apply(type, deltaTime, characterHealth.Character, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index f6da0bfd1..44643c736 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -10,7 +10,8 @@ public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { base.Update(characterHealth, targetLimb, deltaTime); - characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime; + float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier); + characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime; if (Source != null) { characterHealth.BloodlossAffliction.Source = Source; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index a030db96b..b72ed15c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -406,45 +406,53 @@ namespace Barotrauma var root = doc.Root.FromPackage(pathToAppendage.ContentPackage); var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); + //the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing) + //that take up the IDs of this appendage + int idOffset = 0; foreach (var jointElement in root.GetChildElements("joint")) { - if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) + if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; } + + var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams); + Limb attachLimb = null; + if (matchingAffliction.AttachLimbId > -1) { - var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams); - Limb attachLimb = null; - if (matchingAffliction.AttachLimbId > -1) - { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId); - } - else if (matchingAffliction.AttachLimbName != null) - { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName); - } - else if (matchingAffliction.AttachLimbType != LimbType.None) - { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType); - } - if (attachLimb == null) - { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1); - } - if (attachLimb != null) - { - jointParams.Limb1 = attachLimb.Params.ID; - var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams) - { - // Ensure that we have a valid id for the new limb - ID = ragdoll.Limbs.Length - }; - jointParams.Limb2 = appendageLimbParams.ID; - Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams); - huskAppendage.body.Submarine = character.Submarine; - huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation); - ragdoll.AddLimb(huskAppendage); - ragdoll.AddJoint(jointParams); - appendage.Add(huskAppendage); - } + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId); } + else if (matchingAffliction.AttachLimbName != null) + { + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName); + } + else if (matchingAffliction.AttachLimbType != LimbType.None) + { + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType); + } + if (attachLimb == null) + { + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1); + } + if (attachLimb != null) + { + jointParams.Limb1 = attachLimb.Params.ID; + //the joint attaches to a limb outside the character's normal limb count = to another part of the appendage + // -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb + if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count) + { + jointParams.Limb1 += idOffset; + } + var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams); + if (idOffset == 0) + { + idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID; + } + jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length; + Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams); + huskAppendage.body.Submarine = character.Submarine; + huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation); + ragdoll.AddLimb(huskAppendage); + ragdoll.AddJoint(jointParams); + appendage.Add(huskAppendage); + } } return appendage; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index a18f012bc..b9ff8fe06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -214,7 +215,6 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.No)] public Identifier DialogFlag { get; private set; } - [Serialize("", IsPropertySaveable.No)] public Identifier Tag { get; private set; } @@ -276,6 +276,47 @@ namespace Barotrauma } } + public class Description + { + public enum TargetType + { + Any, + Self, + OtherCharacter + } + + public readonly LocalizedString Text; + public readonly Identifier TextTag; + public readonly float MinStrength, MaxStrength; + public readonly TargetType Target; + + public Description(ContentXElement element, AfflictionPrefab affliction) + { + TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty); + if (!TextTag.IsEmpty) + { + Text = TextManager.Get(TextTag); + } + string text = element.GetAttributeString("text", string.Empty); + if (!text.IsNullOrEmpty()) + { + Text = Text?.Fallback(text) ?? text; + } + else if (TextTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions."); + } + + MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f); + MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); + if (MinStrength >= MaxStrength) + { + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min."); + } + Target = element.GetAttributeEnum(nameof(Target), TargetType.Any); + } + } + public class PeriodicEffect { public readonly List StatusEffects = new List(); @@ -313,7 +354,6 @@ namespace Barotrauma public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; public override void Dispose() { } public static IEnumerable List => Prefabs; @@ -330,15 +370,22 @@ namespace Barotrauma //(e.g. mental health problems on head, lack of oxygen on torso...) public readonly LimbType IndicatorLimb; - public readonly LocalizedString Name, Description; + public readonly LocalizedString Name; public readonly Identifier TranslationIdentifier; public readonly bool IsBuff; + public readonly bool AffectMachines; public readonly bool HealableInMedicalClinic; public readonly float HealCostMultiplier; public readonly int BaseHealCost; + public readonly bool ShowBarInHealthMenu; public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; + private readonly LocalizedString defaultDescription; + public readonly ImmutableList Descriptions; + + public readonly bool HideIconAfterDelay; + //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; //how high the strength has to be for the affliction icon to be shown in the UI @@ -355,6 +402,11 @@ namespace Barotrauma //how strong the affliction needs to be before bots attempt to treat it public readonly float TreatmentThreshold = 5.0f; + /// + /// Bots will not try to treat the affliction if the character has any of these afflictions + /// + public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; + /// /// The affliction is automatically removed after this time. 0 = unlimited /// @@ -384,6 +436,8 @@ namespace Barotrauma private readonly ConstructorInfo constructor; + public readonly bool ResetBetweenRounds; + public IEnumerable> TreatmentSuitability { get @@ -411,13 +465,16 @@ namespace Barotrauma { Name = Name.Fallback(fallbackName); } - Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); + defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); string fallbackDescription = element.GetAttributeString("description", ""); if (!string.IsNullOrEmpty(fallbackDescription)) { - Description = Description.Fallback(fallbackDescription); + defaultDescription = defaultDescription.Fallback(fallbackDescription); } - IsBuff = element.GetAttributeBool("isbuff", false); + IsBuff = element.GetAttributeBool(nameof(IsBuff), false); + AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true); + + ShowBarInHealthMenu = element.GetAttributeBool("showbarinhealthmenu", true); HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", !IsBuff && @@ -426,6 +483,8 @@ namespace Barotrauma HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0); + IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty()).ToImmutableHashSet(); + Duration = element.GetAttributeFloat(nameof(Duration), 0.0f); if (element.GetAttribute("nameidentifier") != null) @@ -443,28 +502,33 @@ namespace Barotrauma } } - ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f); - ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); - ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); - MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); - GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); + HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", + ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f); + ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f)); + ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold); + MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); + GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f); + + ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold), Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); - TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); + TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f)); - DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); - BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f); + DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f); + BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f); - KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); + KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f); CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); - IconColors = element.GetAttributeColorArray("iconcolors", null); - AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); - AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", ""); + IconColors = element.GetAttributeColorArray(nameof(IconColors), null); + AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); + AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), ""); + ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); + + List descriptions = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -481,15 +545,38 @@ namespace Barotrauma case "effect": case "periodiceffect": break; + case "description": + descriptions.Add(new Description(subElement, this)); + break; default: DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); break; } } + Descriptions = descriptions.ToImmutableList(); constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + public LocalizedString GetDescription(float strength, Description.TargetType targetType) + { + foreach (var description in Descriptions) + { + if (strength < description.MinStrength || strength > description.MaxStrength) { continue; } + switch (targetType) + { + case Description.TargetType.Self: + if (description.Target == Description.TargetType.OtherCharacter) { continue; } + break; + case Description.TargetType.OtherCharacter: + if (description.Target == Description.TargetType.Self) { continue; } + break; + } + return description.Text; + } + return defaultDescription; + } + public static void LoadAllEffects() { Prefabs.ForEach(p => p.LoadEffects()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 8a569ba1f..91a913cda 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -109,7 +109,7 @@ namespace Barotrauma public bool DoesBleed { - get => Character.Params.Health.DoesBleed; + get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine; private set => Character.Params.Health.DoesBleed = value; } @@ -137,7 +137,7 @@ namespace Barotrauma public bool IsUnconscious { - get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); } + get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); } } public float PressureKillDelay { get; private set; } = 5.0f; @@ -145,9 +145,20 @@ namespace Barotrauma private float vitality; public float Vitality { - get - { - return Character.IsDead ? minVitality : vitality; + get + { + if (Character.IsDead) + { + return minVitality; + } + + if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions)) + { + return Math.Max(vitality, MinVitality + 1); + } + + return vitality; + } private set { @@ -545,7 +556,7 @@ namespace Barotrauma amount -= reduceAmount; if (treatmentAction != null) { - if (treatmentAction.Value == ActionType.OnUse) + if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess) { matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; } @@ -690,10 +701,18 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true) { + if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") + { + if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + { + return; + } + } if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } + if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) @@ -884,25 +903,36 @@ namespace Barotrauma { if (!Character.NeedsOxygen) { return; } + float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); float prevOxygen = OxygenAmount; if (IsUnconscious) { + //clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely) + float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance); //the character dies of oxygen deprivation in 100 seconds after losing consciousness - OxygenAmount = MathHelper.Clamp(OxygenAmount - 1.0f * deltaTime, -100.0f, 100.0f); + OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f); } else { float decreaseSpeed = -5.0f; float increaseSpeed = 10.0f; - float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); decreaseSpeed *= (1f - oxygenlowResistance); increaseSpeed *= (1f + oxygenlowResistance); - OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); + float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier); + if (holdBreathMultiplier <= -1.0f) + { + OxygenAmount = -100.0f; + } + else + { + decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier); + OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); + } } UpdateOxygenProjSpecific(prevOxygen, deltaTime); } - + partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime); partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime); @@ -1078,6 +1108,7 @@ namespace Barotrauma } if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } + if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; } if (ignoreHiddenAfflictions) { @@ -1233,6 +1264,7 @@ namespace Barotrauma var affliction = kvp.Key; var limbHealth = kvp.Value; if (affliction.Strength <= 0.0f || limbHealth != null) { continue; } + if (kvp.Key.Prefab.ResetBetweenRounds) { continue; } healthElement.Add(new XElement("Affliction", new XAttribute("identifier", affliction.Identifier), new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index a71d1e73e..03fb3ad7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -79,27 +79,35 @@ namespace Barotrauma public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; - public DamageModifier(XElement element, string parentDebugName) + public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true) { Deserialize(element); if (element.Attribute("afflictionnames") != null) { DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); } - foreach (var afflictionType in parsedAfflictionTypes) + if (checkErrors) { - if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + foreach (var afflictionType in parsedAfflictionTypes) { - createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); - } - } - foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) - { - if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) - { - createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); + } + } + foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) + { + if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + } + } + if (!parsedAfflictionTypes.Any() && !parsedAfflictionIdentifiers.Any()) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Neither affliction types of identifiers defined."); } } + static void createWarningOrError(string msg) { #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 030e232b1..b79675fa5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -757,6 +757,10 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; + if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0) + { + finalDamageModifier *= character.EmpVulnerability; + } foreach (DamageModifier damageModifier in tempModifiers) { float damageModifierValue = damageModifier.DamageMultiplier; @@ -766,9 +770,13 @@ namespace Barotrauma } finalDamageModifier *= damageModifierValue; } + if (affliction.MultiplyByMaxVitality) + { + finalDamageModifier *= character.MaxVitality / 100f; + } if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f)) { - newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability); + newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction); } else { @@ -778,6 +786,7 @@ namespace Barotrauma { var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character); attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter); + newAffliction = abilityAfflictionCharacter.Affliction; } if (applyAffliction) { @@ -896,6 +905,12 @@ namespace Barotrauma { reEnableTimer = duration; } +#if CLIENT + if (Hidden && LightSource != null) + { + LightSource.Enabled = false; + } +#endif } public void ReEnable() @@ -1189,12 +1204,30 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, character, targets); } else { - if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + + if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory) + { + foreach (Item item in inventory.AllItems) + { + if (statusEffect.TargetIdentifiers != null && + !statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) && + statusEffect.TargetIdentifiers.None(id => item.HasTag(id))) + { + continue; + } + if (statusEffect.TargetSlot > -1) + { + if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; } + } + targets.Add(item); + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 5b45d8882..44018922f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -50,6 +50,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] + public bool IsMachine { get; set; } + [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } @@ -498,6 +501,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool PoisonImmunity { get; set; } + [Serialize(0f, IsPropertySaveable.Yes), Editable] + public float EmpVulnerability { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } @@ -649,7 +655,7 @@ namespace Barotrauma if (HasTag(tag)) { target = null; - DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); + DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); return false; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index 1b3564d0f..ad4b4df25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -1,8 +1,5 @@ -using Microsoft.Xna.Framework; -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -33,7 +30,7 @@ namespace Barotrauma.Abilities NotSelf = 3, Alive = 4, Monster = 5, - InFriendlySubmarine = 6, + InFriendlySubmarine = 6 }; protected List ParseTargetTypes(string[] targetTypeStrings) @@ -41,8 +38,7 @@ namespace Barotrauma.Abilities List targetTypes = new List(); foreach (string targetTypeString in targetTypeStrings) { - TargetType targetType = TargetType.Any; - if (!Enum.TryParse(targetTypeString, true, out targetType)) + if (!Enum.TryParse(targetTypeString, true, out TargetType targetType)) { DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 43a16839d..0ac951fd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -8,11 +9,13 @@ namespace Barotrauma.Abilities { private readonly List targetTypes; - private List conditionals = new List(); + private readonly List conditionals = new List(); public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)); + targetTypes = ParseTargetTypes( + conditionElement.GetAttributeStringArray("targettypes", + conditionElement.GetAttributeStringArray("targettype", Array.Empty()))); foreach (XElement subElement in conditionElement.Elements()) { @@ -28,13 +31,18 @@ namespace Barotrauma.Abilities break; } } + + if (!targetTypes.Any() && !conditionals.Any()) + { + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character."); + } } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityCharacter abilityCharacter) { - if (!(abilityCharacter.Character is Character character)) { return false; } + if (abilityCharacter.Character is not Character character) { return false; } if (!IsViableTarget(targetTypes, character)) { return false; } foreach (var conditional in conditionals) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs new file mode 100644 index 000000000..a79830e5b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs @@ -0,0 +1,19 @@ +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData + { + private readonly Identifier identifier; + + public AbilityConditionCharacterNotLooted(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter ability) { return false; } + + return !ability.Character.MarkedAsLooted.Contains(identifier); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs new file mode 100644 index 000000000..2809f3546 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs @@ -0,0 +1,16 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData + { + public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter targetCharacter) { return false; } + + return targetCharacter.Character.IsUnconscious; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index 5811b3d66..993c19b94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -1,18 +1,26 @@ -using System; +using Barotrauma.Extensions; +using System; +using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { class AbilityConditionItem : AbilityConditionData { - private readonly string[] identifiers; - private readonly string[] tags; + private readonly ImmutableArray identifiers; + private readonly ImmutableArray tags; + private readonly MapEntityCategory category = MapEntityCategory.None; public AbilityConditionItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty(), convertToLowerInvariant: true); - tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + identifiers = conditionElement.GetAttributeIdentifierArray("identifiers", Array.Empty()).ToImmutableArray(); + tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableArray(); + category = conditionElement.GetAttributeEnum("category", MapEntityCategory.None); + + if (identifiers.None() && tags.None() && category == MapEntityCategory.None) + { + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined."); + } } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) @@ -29,6 +37,11 @@ namespace Barotrauma.Abilities if (itemPrefab != null) { + if (category != MapEntityCategory.None) + { + if (!itemPrefab.Category.HasFlag(category)) { return false; } + } + if (identifiers.Any()) { if (!identifiers.Any(t => itemPrefab.Identifier == t)) @@ -36,7 +49,6 @@ namespace Barotrauma.Abilities return false; } } - return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index 9e85d367e..c3d2e872e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -8,6 +8,7 @@ namespace Barotrauma.Abilities { private readonly bool? hasOutpost; private readonly Identifier[] locationIdentifiers; + private readonly bool isPositiveReputation; public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { @@ -16,12 +17,20 @@ namespace Barotrauma.Abilities hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); } locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty()); + + isPositiveReputation = conditionElement.GetAttributeBool("ispositivereputation", false); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityLocation abilityLocation) { + if (isPositiveReputation) + { + if (abilityLocation.Location?.Reputation is not { } reputation) { return false; } + if (reputation.Value <= 0) { return false; } + } + if (locationIdentifiers.Any()) { if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 0e19ec19e..23512f751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -1,38 +1,62 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { class AbilityConditionMission : AbilityConditionData { - private readonly MissionType missionType; + private readonly ImmutableHashSet missionType; + private readonly bool isAffiliated; + public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); - if (!Enum.TryParse(missionTypeString, out missionType)) + string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; + HashSet missionTypes = new HashSet(); + foreach (string missionTypeString in missionTypeStrings) { - DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type."); - return; - } - if (missionType == MissionType.None) - { - DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none."); - return; + if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) + { + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + return; + } + + missionTypes.Add(parsedMission); } + + missionType = missionTypes.ToImmutableHashSet(); + isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilityMission)?.Mission is Mission mission) + if (abilityObject is IAbilityMission { Mission: { } mission }) { - return mission.Prefab.Type == missionType; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); - return false; + if (isAffiliated) + { + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + { + if (amount <= 0) { continue; } + + Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier); + + if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) + { + return true; + } + } + + return false; + } + + return missionType.Contains(mission.Prefab.Type); } + + LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); + return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs index d3ece3bf8..c27404723 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs new file mode 100644 index 000000000..e877c657b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionAllyNearby : AbilityConditionDataless + { + private enum NearbyCharacterTruthy + { + OneCharacterMatches, + NoCharacterMatches + } + + private readonly NearbyCharacterTruthy truthyWhen; + private readonly float distance; + + public AbilityConditionAllyNearby(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + truthyWhen = conditionElement.GetAttributeEnum("truthywhen", NearbyCharacterTruthy.OneCharacterMatches); + distance = conditionElement.GetAttributeFloat("distance", 10f); + } + + protected override bool MatchesConditionSpecific() + { + bool trueCondition = truthyWhen switch + { + NearbyCharacterTruthy.OneCharacterMatches => true, + NearbyCharacterTruthy.NoCharacterMatches => false, + _ => throw new ArgumentOutOfRangeException(nameof(truthyWhen)) + }; + + foreach (Character ally in Character.GetFriendlyCrew(character)) + { + if (ally == character) { continue; } + + float distanceToCharacter = Vector2.DistanceSquared(ally.WorldPosition, character.WorldPosition); + + if (distanceToCharacter < distance * distance) + { + return trueCondition; + } + } + + return !trueCondition; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs new file mode 100644 index 000000000..ede7abe7a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs @@ -0,0 +1,22 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionCrewMemberUnconscious : AbilityConditionDataless + { + public AbilityConditionCrewMemberUnconscious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (!c.IsDead && c.IsUnconscious) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 04d7ebf62..8b20847b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -1,9 +1,5 @@ -using System; -using Barotrauma.Items.Components; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using System; namespace Barotrauma.Abilities { @@ -22,7 +18,7 @@ namespace Barotrauma.Abilities { if (tags.None()) { - return character.GetEquippedItem(null) is Item; + return character.GetEquippedItem(null) != null; } if (requireAll) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs new file mode 100644 index 000000000..f14552583 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionHasLevel : AbilityConditionDataless + { + private readonly Option matchedLevel; + private readonly Option minLevel; + + public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0 + ? Option.Some(match) + : Option.None(); + + minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0 + ? Option.Some(min) + : Option.None(); + + if (matchedLevel.IsNone() && minLevel.IsNone()) + { + throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute."); + } + } + + protected override bool MatchesConditionSpecific() + { + if (matchedLevel.TryUnwrap(out int match)) + { + return character.Info.GetCurrentLevel() == match; + } + + if (minLevel.TryUnwrap(out int min)) + { + return character.Info.GetCurrentLevel() >= min; + } + + return false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 344a580f2..c4017a87f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -1,13 +1,11 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionHasPermanentStat : AbilityConditionDataless { private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float min; + private readonly PermanentStatPlaceholder placeholder; public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { @@ -19,11 +17,14 @@ namespace Barotrauma.Abilities string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); min = conditionElement.GetAttributeFloat("min", 0f); + placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } protected override bool MatchesConditionSpecific() { - return character.Info.GetSavedStatValue(statType, statIdentifier) >= min; + Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); + + return character.Info.GetSavedStatValue(statType, identifier) >= min; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs new file mode 100644 index 000000000..de2f98107 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs @@ -0,0 +1,19 @@ + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasTalent : AbilityConditionDataless + { + private readonly Identifier talentIdentifier; + + public AbilityConditionHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific() + { + bool result = character.HasTalent(talentIdentifier); + return result; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs new file mode 100644 index 000000000..5c6c201de --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Collections.Immutable; + +namespace Barotrauma.Abilities; + +internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless +{ + private readonly ImmutableHashSet tags; + + public AbilityConditionHoldingItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + tags = conditionElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + } + + protected override bool MatchesConditionSpecific() + { + if (tags.Count is 0) + { + return HasItemInHand(character, null); + } + + foreach (Identifier tag in tags) + { + if (HasItemInHand(character, tag)) { return true; } + } + + return false; + + static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) => + character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null || + character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null; + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs new file mode 100644 index 000000000..3cf37c2b9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless + { + public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + int ownLevel = character.Info.GetCurrentLevel(); + + foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (crew == character) { continue; } + + if (crew.Info.GetCurrentLevel() < ownLevel) { return false; } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs new file mode 100644 index 000000000..5d0acdbf0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs @@ -0,0 +1,39 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities; + +internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDataless +{ + private readonly float distance; + private readonly int count; + private readonly ImmutableHashSet targetTypes; + + public AbilityConditionNearbyCharacterCount(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + distance = conditionElement.GetAttributeFloat("distance", 10f); + count = conditionElement.GetAttributeInt("count", 1); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)).ToImmutableHashSet(); + } + + protected override bool MatchesConditionSpecific() + { + int amountNeeded = count; + foreach (Character otherCharacter in Character.CharacterList) + { + if (character.Submarine != otherCharacter.Submarine) { continue; } + if (!IsViableTarget(targetTypes, otherCharacter)) { continue; } + + if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance) + { + amountNeeded--; + + if (amountNeeded <= 0) { return true; } + } + } + + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 9d1e093b2..698e7f207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -15,5 +15,4 @@ namespace Barotrauma.Abilities } public Character Character { get; set; } } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 044d960a9..5a7d22598 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -34,7 +34,7 @@ namespace Barotrauma.Abilities public bool IsViable() { - if (!AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + if (!AllowClientSimulation && GameMain.NetworkMember is { IsClient: true }) { return false; } if (RequiresAlive && Character.IsDead) { return false; } return true; } @@ -67,7 +67,7 @@ namespace Barotrauma.Abilities if (abilityObject is null) { ApplyEffect(); - } + } else { ApplyEffect(abilityObject); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs index e934eb2aa..0b350c514 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs new file mode 100644 index 000000000..c7ef4461e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs @@ -0,0 +1,35 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityApplyStatusEffectToNonHumans : CharacterAbilityApplyStatusEffects + { + private readonly float maxDistance; + + public CharacterAbilityApplyStatusEffectToNonHumans(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + } + + protected override void ApplyEffect() + { + foreach (Character character in Character.CharacterList) + { + if (character.IsHuman) { continue; } + + if (maxDistance < float.MaxValue) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } + } + ApplyEffectSpecific(character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 5711c0ed5..969c9c5bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -17,6 +17,8 @@ namespace Barotrauma.Abilities readonly List targets = new List(); + private bool effectBeingApplied; + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); @@ -29,44 +31,57 @@ namespace Barotrauma.Abilities protected void ApplyEffectSpecific(Character targetCharacter) { - foreach (var statusEffect in statusEffects) + //prevent an infinite loop if an effect triggers itself + //(e.g. a talent that triggers when an affliction is applied, and applies that same affliction) + if (effectBeingApplied) { return; } + + effectBeingApplied = true; + + try { - if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) + foreach (var statusEffect in statusEffects) { - // currently used to spawn items on the targeted character - statusEffect.SetUser(targetCharacter); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); - if (!nearbyCharactersAppliesToSelf) + if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - targets.RemoveAll(c => c == Character); + // currently used to spawn items on the targeted character + statusEffect.SetUser(targetCharacter); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); } - if (!nearbyCharactersAppliesToAllies) + else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + targets.Clear(); + statusEffect.AddNearbyTargets(targetCharacter.WorldPosition, targets); + if (!nearbyCharactersAppliesToSelf) + { + targets.RemoveAll(c => c == Character); + } + if (!nearbyCharactersAppliesToAllies) + { + targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + } + if (!nearbyCharactersAppliesToEnemies) + { + targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + } + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); } - if (!nearbyCharactersAppliesToEnemies) + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { - targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } + else + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) - { - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); - } - else - { - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } } + finally + { + effectBeingApplied = false; + } } protected override void ApplyEffect() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 70b871963..d44c24194 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System.Collections.Immutable; namespace Barotrauma.Abilities { @@ -6,11 +7,18 @@ namespace Barotrauma.Abilities { private readonly bool allowSelf; private readonly float maxDistance = float.MaxValue; + private readonly bool inSameRoom; + private readonly ImmutableHashSet jobIdentifiers; + + public override bool AllowClientSimulation { get; } public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { allowSelf = abilityElement.GetAttributeBool("allowself", true); maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + inSameRoom = abilityElement.GetAttributeBool("insameroom", false); + jobIdentifiers = abilityElement.GetAttributeIdentifierImmutableHashSet("jobs", ImmutableHashSet.Empty); + AllowClientSimulation = abilityElement.GetAttributeBool("allowclientsimulation", true); } @@ -19,6 +27,27 @@ namespace Barotrauma.Abilities foreach (Character character in Character.GetFriendlyCrew(Character)) { if (!allowSelf && character == Character) { continue; } + + if (!jobIdentifiers.IsEmpty) + { + bool hadJob = false; + foreach (Identifier job in jobIdentifiers) + { + if (character.HasJob(job.Value)) + { + hadJob = true; + break; + } + } + + if (!hadJob) { continue; } + } + + if (inSameRoom && !character.IsInSameRoomAs(Character)) + { + continue; + } + if (maxDistance < float.MaxValue) { if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs new file mode 100644 index 000000000..27d4afe94 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs @@ -0,0 +1,63 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityApplyStatusEffectsToApprenticeship : CharacterAbilityApplyStatusEffects + { + private readonly bool invert; + private readonly ImmutableHashSet jobPrefabList = JobPrefab.Prefabs.ToImmutableHashSet(); + + public CharacterAbilityApplyStatusEffectsToApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + invert = abilityElement.GetAttributeBool("invert", false); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList); + if (apprenticeJob is null) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + return; + } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + JobPrefab? characterJob = character.Info?.Job?.Prefab; + if (characterJob is null) { continue; } + + switch (characterJob.Identifier == apprenticeJob.Identifier) + { + case true when invert: + continue; + case false when !invert: + continue; + } + + ApplyEffectSpecific(character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + + public static JobPrefab? GetApprenticeJob(Character character, IReadOnlyCollection jobList) + { + foreach (JobPrefab prefab in jobList) + { + if (character.Info.GetSavedStatValue(StatTypes.Apprenticeship, prefab.Identifier) > 0) + { + return prefab; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 1204769de..0cd2b4857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -1,17 +1,21 @@ -using Microsoft.Xna.Framework; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGainSimultaneousSkill : CharacterAbility { private readonly Identifier skillIdentifier; - private readonly bool ignoreAbilitySkillGain; + + private readonly bool ignoreAbilitySkillGain, + targetAllies; public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); + targetAllies = abilityElement.GetAttributeBool("targetallies", false); + if (skillIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined."); + } } protected override void ApplyEffect(AbilityObject abilityObject) @@ -19,7 +23,19 @@ namespace Barotrauma.Abilities if (abilityObject is AbilitySkillGain abilitySkillGain) { if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; } - Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true); + Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier; + if (targetAllies) + { + foreach (Character otherCharacter in Character.GetFriendlyCrew(Character)) + { + if (otherCharacter == Character) { continue; } + otherCharacter.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); + } + } + else + { + Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 9dcab6bb6..13bae5e96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGiveAffliction : CharacterAbility { @@ -18,7 +16,7 @@ namespace Barotrauma.Abilities if (afflictionId.IsEmpty) { - DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set."); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs new file mode 100644 index 000000000..5686f777a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -0,0 +1,53 @@ +namespace Barotrauma.Abilities; + +internal sealed class CharacterAbilityGiveExperience : CharacterAbility +{ + public override bool AppliesEffectOnIntervalUpdate => true; + + private readonly int amount; + private readonly int level; + + public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + level = abilityElement.GetAttributeInt("level", 0); + + if (amount == 0 && level == 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}."); + } + if (amount > 0 && level > 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level."); + } + } + + private void ApplyEffectSpecific(Character targetCharacter) + { + if (amount != 0) + { + targetCharacter.Info?.GiveExperience(amount); + } + if (level > 0) + { + targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level)); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffectSpecific(Character); + } + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs index e56bee86c..a315a3328 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGiveFlag : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs new file mode 100644 index 000000000..4fe2c1c86 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveItemStat : CharacterAbility + { + private readonly ItemTalentStats stat; + private readonly float value; + + public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityItem ability) { return; } + + ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs new file mode 100644 index 000000000..1b4f880f8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -0,0 +1,41 @@ +#nullable enable + +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveItemStatToTags: CharacterAbility + { + private readonly ItemTalentStats stat; + private readonly float value; + private readonly ImmutableHashSet tags; + + public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); + value = abilityElement.GetAttributeFloat("value", 0f); + tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect() + { + if (Character?.Submarine is null) { return; } + + foreach (Item item in Character.Submarine.GetItems(true)) + { + if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) + { + item.StatManager.ApplyStat(stat, value, CharacterTalent); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index 40148524c..a45e92b1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGiveMoney : CharacterAbility { @@ -13,6 +11,11 @@ namespace Barotrauma.Abilities { amount = abilityElement.GetAttributeInt("amount", 0); scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty); + + if (amount == 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0."); + } } private void ApplyEffectSpecific(Character targetCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index a0750d5d4..b62f56296 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -1,11 +1,15 @@ -using Barotrauma.Extensions; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { + public enum PermanentStatPlaceholder + { + None, + LocationName, + LocationIndex + } + class CharacterAbilityGivePermanentStat : CharacterAbility { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float value; private readonly float maxValue; @@ -13,6 +17,7 @@ namespace Barotrauma.Abilities private readonly bool removeOnDeath; private readonly bool giveOnAddingFirstTime; private readonly bool setValue; + private readonly PermanentStatPlaceholder placeholder; //private readonly float maximumValue; public override bool AllowClientSimulation => true; @@ -20,7 +25,11 @@ namespace Barotrauma.Abilities public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); + if (statIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined."); + } string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); @@ -29,6 +38,7 @@ namespace Barotrauma.Abilities removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false); giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); + placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } public override void InitializeAbility(bool addingFirstTime) @@ -51,14 +61,33 @@ namespace Barotrauma.Abilities private void ApplyEffectSpecific() { + Identifier identifier = HandlePlaceholders(placeholder, statIdentifier); if (targetAllies) { - Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue)); + foreach (Character c in Character.GetFriendlyCrew(Character)) + { + c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + } } else { - Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } + + public static Identifier HandlePlaceholders(PermanentStatPlaceholder placeholder, Identifier original) + { + if (GameMain.GameSession?.Campaign?.Map is not { } map) { return original; } + + switch (placeholder) + { + case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location: + return original.Replace("[placeholder]", location.Name); + case PermanentStatPlaceholder.LocationIndex: + return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString()); + } + + return original; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs new file mode 100644 index 000000000..6d3777d53 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs @@ -0,0 +1,39 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveReputation : CharacterAbility + { + private readonly Identifier factionIdentifier; + private readonly float amount; + + public CharacterAbilityGiveReputation(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + amount = abilityElement.GetAttributeFloat("amount", 0f); + if (factionIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined."); + } + if (amount == 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0."); + } + } + + protected override void ApplyEffect() + { + if (GameMain.GameSession?.Campaign is not { } campaign) { return; } + + foreach (Faction faction in campaign.Factions) + { + if (faction.Prefab.Identifier != factionIdentifier) { continue; } + + faction.Reputation.AddReputation(amount); + break; + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) => ApplyEffect(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 347f69a25..9df7fc87b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGiveResistance : CharacterAbility { @@ -10,17 +8,23 @@ namespace Barotrauma.Abilities public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty)); - multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency + multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); if (resistanceId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); } + if (MathUtils.NearlyEqual(multiplier, 1)) + { + DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing."); + } + } public override void InitializeAbility(bool addingFirstTime) { - Character.ChangeAbilityResistance(resistanceId, multiplier); + TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier); + Character.ChangeAbilityResistance(identifier, multiplier); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs index a97ec2ee4..4cc9d37a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGiveStat : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs index 1eed1afae..e61e3981b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs @@ -1,7 +1,4 @@ -using Barotrauma.Extensions; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGiveTalentPoints : CharacterAbility { @@ -9,7 +6,11 @@ namespace Barotrauma.Abilities public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - amount = abilityElement.GetAttributeInt("amount", 0); + amount = abilityElement.GetAttributeInt("amount", 0); + if (amount == 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0."); + } } public override void InitializeAbility(bool addingFirstTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs new file mode 100644 index 000000000..2b4dd4cac --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs @@ -0,0 +1,29 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveTalentPointsToAllies : CharacterAbility + { + private readonly int amount; + + public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + if (amount == 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0."); + } + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.Info is null) { return; } + character.Info.AdditionalTalentPoints += amount; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index cee2198ff..2e1816bd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -1,6 +1,4 @@ using Barotrauma.Extensions; -using Microsoft.Xna.Framework; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs new file mode 100644 index 000000000..45ddb19fb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs @@ -0,0 +1,22 @@ +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityMarkAsLooted: CharacterAbility + { + private readonly Identifier identifier; + public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}."); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter { Character: { } character }) { return; } + + character.MarkedAsLooted.Add(identifier); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index 97b0302bd..6c275b1c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -1,30 +1,36 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyAffliction : CharacterAbility { - private readonly string[] afflictionIdentifiers; + private readonly Identifier[] afflictionIdentifiers; + + private readonly Identifier replaceWith; private readonly float addedMultiplier; public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); + afflictionIdentifiers = abilityElement.GetAttributeIdentifierArray("afflictionidentifiers", System.Array.Empty()); + replaceWith = abilityElement.GetAttributeIdentifier("replacewith", Identifier.Empty); addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + var abilityAffliction = abilityObject as IAbilityAffliction; + if (abilityAffliction?.Affliction is Affliction affliction) { - foreach (string afflictionIdentifier in afflictionIdentifiers) + foreach (Identifier afflictionIdentifier in afflictionIdentifiers) { - if (affliction.Identifier == afflictionIdentifier) + if (affliction.Identifier != afflictionIdentifier) { continue; } + affliction.Strength *= 1 + addedMultiplier; + if (!replaceWith.IsEmpty) { - affliction.Strength *= 1 + addedMultiplier; - } + if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab)) + { + abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength); + } + } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index a6141b79f..686aa6d48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs index 52f47c471..ff7ac0995 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs @@ -1,7 +1,4 @@ -using System; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyFlag : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs index ed5a5a35f..3c1ec2272 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -1,23 +1,25 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyResistance : CharacterAbility { private readonly Identifier resistanceId; - private readonly float resistance; + private readonly float multiplier; bool lastState; public override bool AllowClientSimulation => true; // should probably be split to different classes public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", ""); - resistance = abilityElement.GetAttributeFloat("resistance", 1f); + resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty)); + multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); if (resistanceId.IsEmpty) { - DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}."); + } + if (MathUtils.NearlyEqual(multiplier, 1.0f)) + { + DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing."); } } @@ -25,7 +27,15 @@ namespace Barotrauma.Abilities { if (conditionsMatched != lastState) { - Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance); + TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier); + if (conditionsMatched) + { + Character.ChangeAbilityResistance(identifier, multiplier); + } + else + { + Character.RemoveAbilityResistance(identifier); + } lastState = conditionsMatched; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs index c7f792475..a1f69c328 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyStat : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs index a3141b037..16979f552 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs index 26cec9b48..b02b85e1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 6970c2e6d..57ed31b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyValue : CharacterAbility { @@ -11,6 +9,10 @@ namespace Barotrauma.Abilities { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); + if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f)) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing."); + } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs index 10c1ddcd1..ba7ef06eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityPutItem : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs new file mode 100644 index 000000000..14a84d337 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -0,0 +1,27 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityReduceAffliction : CharacterAbility + { + private readonly Identifier afflictionId; + private readonly float amount; + + public CharacterAbilityReduceAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); + amount = abilityElement.GetAttributeFloat("amount", 0); + + if (afflictionId.IsEmpty) + { + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set."); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter character) { return; } + character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs new file mode 100644 index 000000000..78d7f501a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs @@ -0,0 +1,19 @@ +#nullable enable + +using Barotrauma.Items.Components; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility + { + public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } + + int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); + ingredients.Items.RemoveAt(randomIndex); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 1660c54e1..3e85a16dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -1,16 +1,19 @@ -using System.Xml.Linq; - + namespace Barotrauma.Abilities { class CharacterAbilityResetPermanentStat : CharacterAbility { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; public override bool AppliesEffectOnIntervalUpdate => true; public override bool AllowClientSimulation => true; public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); + if (statIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty."); + } } protected override void ApplyEffect(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs deleted file mode 100644 index 21d679bd2..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Xna.Framework; -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class CharacterAbilityRevive : CharacterAbility - { - public override bool AppliesEffectOnIntervalUpdate => true; - - public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - } - - private void ApplyEffectSpecific() - { - Character.Revive(removeAllAfflictions: false); - } - - protected override void ApplyEffect() - { - ApplyEffectSpecific(); - } - - protected override void ApplyEffect(AbilityObject abilityObject) - { - ApplyEffectSpecific(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs new file mode 100644 index 000000000..3953cce9f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs @@ -0,0 +1,38 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilitySetMetadataInt : CharacterAbility + { + private readonly Identifier identifier; + private readonly int value; + + public CharacterAbilitySetMetadataInt(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + value = abilityElement.GetAttributeInt("value", 0); + if (identifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty."); + } + } + + public override void InitializeAbility(bool addingFirstTime) + { + ApplyEffect(); + } + + protected override void ApplyEffect() + { + if (identifier == Identifier.Empty) { return; } + if (GameMain.GameSession?.Campaign?.CampaignMetadata is not { } metadata) { return; } + + metadata.SetValue(identifier, value); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs index 4d6647f55..e3d1676d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs deleted file mode 100644 index e8d0ad788..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Barotrauma.Abilities -{ - class CharacterAbilityUnlockTree : CharacterAbility - { - public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - } - - public override void InitializeAbility(bool addingFirstTime) - { - if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - var subTree = talentTree.TalentSubTrees.Find(t => t.AllTalentIdentifiers.Contains(CharacterTalent.Prefab.Identifier)); - if (subTree == null) { return; } - - subTree.ForceUnlock = true; - if (!addingFirstTime) { return; } - - foreach (var talentId in subTree.AllTalentIdentifiers) - { - if (talentId == CharacterTalent.Prefab.Identifier) { continue; } - if (Character.GiveTalent(talentId)) - { - Character.Info.AdditionalTalentPoints++; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs index 9a67d1fd3..87a63db20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index e212b3224..c7a549aa5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApprenticeship : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs index 745cb706e..4bc16e754 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs @@ -1,20 +1,17 @@ using System; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { class CharacterAbilityAtmosMachine : CharacterAbility { private readonly float addedValue; - private readonly float multiplyValue; private readonly Identifier[] tags; private readonly int maxMultiplyCount; public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); - multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty()); maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs index 75aa86835..77d765a57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityBountyHunter : CharacterAbility { - private float vitalityPercentage; + private readonly float vitalityPercentage; public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index c3c99c080..29f0cf0cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs index 24b2a02e2..042edb00e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -1,7 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityMultitasker : CharacterAbility { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs index c9a160923..5f8b94591 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs @@ -1,12 +1,10 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityPsychoClown : CharacterAbility { - private StatTypes statType; - private float maxValue; - private string afflictionIdentifier; + private readonly StatTypes statType; + private readonly float maxValue; + private readonly string afflictionIdentifier; private float lastValue = 0f; public override bool AllowClientSimulation => true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs index 85ef28d54..75d474784 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -1,8 +1,5 @@ using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs new file mode 100644 index 000000000..ca81f0634 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -0,0 +1,50 @@ +#nullable enable + +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityUnlockApprenticeshipTalentTree : CharacterAbility + { + public override bool AllowClientSimulation => false; + + public CharacterAbilityUnlockApprenticeshipTalentTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + + JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); + if (apprentice is null) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + return; + } + + if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } + + HashSet> talentsTrees = new HashSet>(); + foreach (TalentSubTree subTree in talentTree.TalentSubTrees) + { + if (subTree.Type != TalentTreeType.Specialization) { continue; } + talentsTrees.Add(subTree.AllTalentIdentifiers); + } + + ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); + + foreach (Identifier identifier in selectedTalentTree) + { + if (Character.HasTalent(identifier)) { continue; } + + Character.GiveTalent(identifier); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index bd4b45998..766b48ea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -18,12 +18,15 @@ namespace Barotrauma.Abilities protected readonly int maxTriggerCount; protected int timesTriggered = 0; - - // add support for OR conditions? + // add support for OR conditions? protected readonly List abilityConditions = new List(); - // separate dictionaries for each type of characterability? - protected readonly List characterAbilities = new List(); + /// + /// List of abilities that are triggered by this group. + /// Fallback abilities are triggered if the conditional fails + /// + protected readonly List characterAbilities = new List(), + fallbackAbilities = new List(); public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { @@ -38,6 +41,9 @@ namespace Barotrauma.Abilities case "abilities": LoadAbilities(subElement); break; + case "fallbackabilities": + LoadFallbackAbilities(subElement); + break; case "conditions": LoadConditions(subElement); break; @@ -47,10 +53,23 @@ namespace Barotrauma.Abilities public void ActivateAbilityGroup(bool addingFirstTime) { + if (!CheckActivatingCondition()) { return; } + foreach (var characterAbility in characterAbilities) { characterAbility.InitializeAbility(addingFirstTime); } + + foreach (var characterAbility in fallbackAbilities) + { + characterAbility.InitializeAbility(addingFirstTime); + } + } + + private bool CheckActivatingCondition() + { + if (AbilityEffectType is not AbilityEffectType.None) { return true; } + return !abilityConditions.Any(static abilityCondition => !abilityCondition.MatchesCondition()); } public void LoadConditions(ContentXElement conditionElements) @@ -85,6 +104,17 @@ namespace Barotrauma.Abilities characterAbilities.Add(characterAbility); } + public void AddFallbackAbility(CharacterAbility characterAbility) + { + if (characterAbility == null) + { + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + fallbackAbilities.Add(characterAbility); + } + // XML private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true) { @@ -135,6 +165,14 @@ namespace Barotrauma.Abilities } } + private void LoadFallbackAbilities(ContentXElement abilityElements) + { + foreach (var abilityElementGroup in abilityElements.Elements()) + { + AddFallbackAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); + } + } + private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent) { CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index b77f4332d..b9418955f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -1,29 +1,38 @@ -namespace Barotrauma.Abilities +using System.Collections.Generic; + +namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { - public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : + public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { } public void CheckAbilityGroup(AbilityObject abilityObject) { if (!IsActive) { return; } - if (IsApplicable(abilityObject)) + + if (IsOverTriggerCount) { return; } + + List abilities = IsApplicable(abilityObject) ? characterAbilities : fallbackAbilities; + + foreach (CharacterAbility characterAbility in abilities) { - foreach (var characterAbility in characterAbilities) + if (characterAbility.IsViable()) { - if (characterAbility.IsViable()) - { - characterAbility.ApplyAbilityEffect(abilityObject); - } + characterAbility.ApplyAbilityEffect(abilityObject); } + } + + if (abilities.Count > 0) + { timesTriggered++; } } + private bool IsOverTriggerCount => timesTriggered >= maxTriggerCount; + private bool IsApplicable(AbilityObject abilityObject) { - if (timesTriggered >= maxTriggerCount) { return false; } foreach (var abilityCondition in abilityConditions) { if (!abilityCondition.MatchesCondition(abilityObject)) @@ -31,7 +40,8 @@ return false; } } + return true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 7cc1e24eb..cf1100db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,4 +1,7 @@ -namespace Barotrauma.Abilities +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { @@ -9,48 +12,72 @@ private float effectDelayTimer; - public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : + public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) - { + { // too many overlapping intervals could cause hitching? maybe randomize a little interval = abilityElementGroup.GetAttributeFloat("interval", 0f); effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f); } + public void UpdateAbilityGroup(float deltaTime) { if (!IsActive) { return; } - TimeSinceLastUpdate += deltaTime; - if (TimeSinceLastUpdate >= interval) - { - bool conditionsMatched = IsApplicable(); - effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f; - conditionsMatched &= effectDelayTimer >= effectDelay; - foreach (var characterAbility in characterAbilities) - { - if (characterAbility.IsViable()) - { - characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); - } - } - if (conditionsMatched) - { - timesTriggered++; - } - TimeSinceLastUpdate = 0; + TimeSinceLastUpdate += deltaTime; + if (TimeSinceLastUpdate < interval) { return; } + + bool conditionsMatched; + + if (AllConditionsMatched()) + { + effectDelayTimer += TimeSinceLastUpdate; + bool shouldApplyDelayedEffect = effectDelayTimer >= effectDelay; + conditionsMatched = shouldApplyDelayedEffect; } + else + { + effectDelayTimer = 0f; + conditionsMatched = false; + } + + bool hasFallbacks = fallbackAbilities.Count > 0; + + List abilitiesToRun = + !conditionsMatched && hasFallbacks + ? fallbackAbilities + : characterAbilities; + + if (hasFallbacks) + { + conditionsMatched = true; + } + + foreach (var characterAbility in abilitiesToRun) + { + if (!characterAbility.IsViable()) { continue; } + + characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); + } + + if (conditionsMatched) + { + timesTriggered++; + } + + TimeSinceLastUpdate = 0; } - private bool IsApplicable() + + private bool AllConditionsMatched() { if (timesTriggered >= maxTriggerCount) { return false; } + foreach (var abilityCondition in abilityConditions) { - if (!abilityCondition.MatchesCondition()) - { - return false; - } + if (!abilityCondition.MatchesCondition()) { return false; } } + return true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 0cf4b3420..4cc374e79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -19,6 +19,7 @@ namespace Barotrauma // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well public List UnlockedRecipes { get; } = new List(); + public List UnlockedStoreItems { get; } = new List(); public CharacterTalent(TalentPrefab talentPrefab, Character character) { @@ -45,7 +46,17 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier); + DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}"); + } + break; + case "addedstoreitem": + if (subElement.GetAttributeIdentifier("itemtag", Identifier.Empty) is { IsEmpty: false } storeItemTag) + { + UnlockedStoreItems.Add(storeItemTag); + } + else + { + DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}"); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index d8f954873..d3cc70e56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; +#if CLIENT +using Microsoft.Xna.Framework; +#endif namespace Barotrauma { @@ -14,6 +14,10 @@ namespace Barotrauma public readonly Sprite Icon; +#if CLIENT + public readonly Option ColorOverride; +#endif + public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); public ContentXElement ConfigElement @@ -28,8 +32,22 @@ namespace Barotrauma DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); - Description = ""; - + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); + if (!nameIdentifier.IsEmpty) + { + DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); + } + + Description = string.Empty; + +#if CLIENT + Color colorOverride = element.GetAttributeColor("coloroverride", Color.TransparentBlack); + + ColorOverride = colorOverride != Color.TransparentBlack + ? Option.Some(colorOverride) + : Option.None(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index e3379314f..dc93405b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -5,9 +5,9 @@ using System.Linq; namespace Barotrauma { - class TalentTree : Prefab + internal sealed class TalentTree : Prefab { - public enum TalentTreeStageState + public enum TalentStages { Invalid, Locked, @@ -40,16 +40,17 @@ namespace Barotrauma DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); return; } - + List subTrees = new List(); foreach (var subTreeElement in element.GetChildElements("subtree")) { subTrees.Add(new TalentSubTree(subTreeElement)); } + TalentSubTrees = subTrees.ToImmutableArray(); AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet(); } - + public bool TalentIsInTree(Identifier talentIdentifier) { return AllTalentIdentifiers.Contains(talentIdentifier); @@ -57,31 +58,44 @@ namespace Barotrauma public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier) { - return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection)Array.Empty()); + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection)Array.Empty()); + } + + public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection selectedTalents) + { + IEnumerable blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)), + requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier)); + + return requiredSubTrees.All(tst => tst.HasEnoughTalents(selectedTalents)) && // check if we meet requirements + !blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents) && !tst.HasMaxTalents(selectedTalents)); // check if any other talent trees are blocking this one } // i hate this function - markus // me too - joonas - public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List selectedTalents) + public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection selectedTalents) { - if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } + if (character?.Info?.Job.Prefab is null) { return TalentStages.Invalid; } - if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentStages.Invalid; } - TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + if (subTree is null) { return TalentStages.Invalid; } - if (subTree == null) { return TalentTreeStageState.Invalid; } + if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) + { + return TalentStages.Locked; + } TalentOption targetTalentOption = subTree.TalentOptionStages[index]; - if (targetTalentOption.TalentIdentifiers.Any(t => character.HasTalent(t))) + if (targetTalentOption.HasEnoughTalents(character.Info)) { - return TalentTreeStageState.Unlocked; + return TalentStages.Unlocked; } - if (targetTalentOption.TalentIdentifiers.Any(t => selectedTalents.Contains(t))) + if (targetTalentOption.HasSelectedTalent(selectedTalents)) { - return TalentTreeStageState.Highlighted; + return TalentStages.Highlighted; } bool hasTalentInLastTier = true; @@ -91,55 +105,46 @@ namespace Barotrauma if (lastindex >= 0) { TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex]; - hasTalentInLastTier = lastLatentOption.TalentIdentifiers.Any(HasTalent); - isLastTalentPurchased = lastLatentOption.TalentIdentifiers.Any(t => character.HasTalent(t)); + hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents); + isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info); } if (!hasTalentInLastTier) { - return TalentTreeStageState.Locked; + return TalentStages.Locked; } bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0; if (hasPointsForNewTalent) { - return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available; + return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available; } - return TalentTreeStageState.Locked; - - bool HasTalent(Identifier talentId) - { - return selectedTalents.Contains(talentId); - } + return TalentStages.Locked; } - public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection selectedTalents) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection selectedTalents) { if (character?.Info?.Job.Prefab == null) { return false; } - if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } - + if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } - foreach (var subTree in talentTree.TalentSubTrees) + foreach (var subTree in talentTree!.TalentSubTrees) { - if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.TalentIdentifiers.Contains(talentIdentifier))) { return true; } + if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; } foreach (var talentOptionStage in subTree.TalentOptionStages) { - bool hasTalentInThisTier = talentOptionStage.TalentIdentifiers.Any(t => selectedTalents.Contains(t)); - if (!hasTalentInThisTier) + if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) { - if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) - { - return true; - } - else - { - break; - } + return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); + } + bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents); + if (!optionStageCompleted) + { + break; } } } @@ -164,60 +169,153 @@ namespace Barotrauma } } } + return viableTalents; } public override void Dispose() { } } - class TalentSubTree + internal enum TalentTreeType + { + Specialization, + Primary + } + + internal sealed class TalentSubTree { public Identifier Identifier { get; } public LocalizedString DisplayName { get; } - public bool ForceUnlock; - public readonly ImmutableArray TalentOptionStages; public readonly ImmutableHashSet AllTalentIdentifiers; + public readonly TalentTreeType Type; + public readonly ImmutableHashSet RequiredTrees; + public readonly ImmutableHashSet BlockedTrees; + + public bool HasEnoughTalents(IReadOnlyCollection talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents)); + public bool HasMaxTalents(IReadOnlyCollection talents) => TalentOptionStages.All(option => option.HasMaxTalents(talents)); + public bool HasAnyTalent(IReadOnlyCollection talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents)); + public TalentSubTree(ContentXElement subTreeElement) { Identifier = subTreeElement.GetAttributeIdentifier("identifier", ""); - DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value); + string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty); + if (string.IsNullOrWhiteSpace(nameIdentifier)) + { + nameIdentifier = $"talenttree.{Identifier}"; + } + DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); + Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization); + RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet.Empty); + BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet.Empty); List talentOptionStages = new List(); foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } + TalentOptionStages = talentOptionStages.ToImmutableArray(); AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet(); } - } - class TalentOption + internal readonly struct TalentOption { private readonly ImmutableHashSet talentIdentifiers; public IEnumerable TalentIdentifiers => talentIdentifiers; - public bool HasTalent(Identifier talentIdentifier) + /// + /// How many talents need to be unlocked to consider this tree completed + /// + public readonly int RequiredTalents; + /// + /// How many talents can be unlocked in total + /// + public readonly int MaxChosenTalents; + + /// + /// When specified the talent option will show talent with this identifier + /// and clicking on it will expand the talent option to show the talents + /// + public readonly Dictionary> ShowCaseTalents = new Dictionary>(); + + public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= RequiredTalents; + public bool HasEnoughTalents(IReadOnlyCollection selectedTalents) => CountMatchingTalents(selectedTalents) >= RequiredTalents; + public bool HasMaxTalents(IReadOnlyCollection selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents; + + // No LINQ + public bool HasSelectedTalent(IReadOnlyCollection selectedTalents) { - return talentIdentifiers.Contains(talentIdentifier); + foreach (Identifier talent in selectedTalents) + { + if (talentIdentifiers.Contains(talent)) + { + return true; + } + } + return false; + } + + public int CountMatchingTalents(IReadOnlyCollection talents) + { + int i = 0; + foreach (Identifier talent in talents) + { + if (talentIdentifiers.Contains(talent)) + { + i++; + } + } + return i; } public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) { - var talentIdentifiers = new HashSet(); - foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1); + RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents); + + if (RequiredTalents > MaxChosenTalents) { - Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); - talentIdentifiers.Add(identifier); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents."); + } + + HashSet identifiers = new HashSet(); + foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements()) + { + Identifier elementName = talentOptionElement.Name.ToIdentifier(); + if (elementName == "talentoption") + { + identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty)); + } + else if (elementName == "showcasetalent") + { + Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + HashSet showCaseTalentIdentifiers = new HashSet(); + foreach (ContentXElement subElement in talentOptionElement.Elements()) + { + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + showCaseTalentIdentifiers.Add(identifier); + identifiers.Add(identifier); + } + ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet()); + } + } + + talentIdentifiers = identifiers.ToImmutableHashSet(); + + if (RequiredTalents > talentIdentifiers.Count) + { + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage."); + } + if (MaxChosenTalents > talentIdentifiers.Count) + { + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents."); } - this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet(); } } - -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 736f5053e..15d0bb76a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -67,10 +67,10 @@ namespace Barotrauma .ToImmutableHashSet(); } - public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - static Result fail(string error, Exception? exception = null) - => Result.Failure(new LoadError(error, exception)); + static Result fail(string error, Exception? exception = null) + => Result.Failure(new ContentPackage.LoadError(error, exception)); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); @@ -83,6 +83,8 @@ namespace Barotrauma { return fail($"No content path defined for file of type \"{elemName}\""); } + + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); try { filePath = type.MutateContentPath(filePath); @@ -90,10 +92,16 @@ namespace Barotrauma { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); } + var file = type.CreateInstance(contentPackage, filePath); - return file is null - ? throw new Exception($"Content type is not implemented correctly") - : Result.Success(file); + if (file is null) { return fail($"Content type {type.Type.Name} is not implemented correctly"); } + + if (errorCatcher.Errors.Any()) + { + return fail( + $"Errors were issued to the debug console when loading \"{filePath}\" of type \"{elemName}\""); + } + return Result.Success(file); } catch (Exception e) { @@ -123,23 +131,5 @@ namespace Barotrauma } public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); - - public readonly struct LoadError - { - public readonly string Message; - public readonly Exception? Exception; - - public LoadError(string message, Exception? exception) - { - Message = message; - Exception = exception; - } - - public override string ToString() - => Message - + (Exception is { StackTrace: var stackTrace } - ? '\n' + stackTrace.CleanupStackTrace() - : string.Empty); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index d99d29b57..a53d13328 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; using System.Xml.Linq; @@ -14,11 +13,20 @@ namespace Barotrauma { public abstract class ContentPackage { + public readonly record struct LoadError(string Message, Exception? Exception) + { + public override string ToString() + => Message + + (Exception is { StackTrace: var stackTrace } + ? '\n' + stackTrace.CleanupStackTrace() + : string.Empty); + } + public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( - SaveUtil.SaveFolder, + SaveUtil.DefaultSaveFolder, "WorkshopMods", "Installed"); @@ -37,12 +45,30 @@ namespace Barotrauma public readonly Option InstallTime; public ImmutableArray Files { get; private set; } - public ImmutableArray Errors { get; private set; } + + /// + /// Errors that occurred when loading this content package. + /// Currently, all errors are considered fatal and the game + /// will refuse to load a content package that has any errors. + /// + public ImmutableArray FatalLoadErrors { get; private set; } + + /// + /// An error that occurred when trying to enable this mod. + /// This field doesn't directly affect whether or not this mod + /// can be enabled, but if it's been set to anything other than + /// Option.None then the game has already refused to enable it + /// at least once. + /// + public Option EnableError { get; private set; } + = Option.None; + + public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome(); public async Task IsUpToDate() { if (!UgcId.TryUnwrap(out var ugcId)) { return true; } - if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; } + if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; } if (!InstallTime.TryUnwrap(out var installTime)) { return true; } Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); @@ -55,20 +81,25 @@ namespace Barotrauma /// /// Does the content package include some content that needs to match between all players in multiplayer. /// - public bool HasMultiplayerSyncedContent { get; private set; } + public bool HasMultiplayerSyncedContent { get; } protected ContentPackage(XDocument doc, string path) { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + Path = path.CleanUpPathCrossPlatform(); XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); Name = rootElement.GetAttributeString("name", "").Trim(); AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty()) .Select(n => n.Trim()).ToImmutableArray(); - AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); - UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); - + + if (Name.IsNullOrWhiteSpace() && AltNames.Any()) + { + Name = AltNames.First(); + } + UgcId = steamWorkshopId != 0 ? Option.Some(new SteamWorkshopId(steamWorkshopId)) : Option.None(); @@ -85,23 +116,31 @@ namespace Barotrauma .ToArray(); Files = fileResults - .OfType>() - .Select(f => f.Value) + .Successes() .ToImmutableArray(); - Errors = fileResults - .OfType>() - .Select(f => f.Error) + FatalLoadErrors = fileResults + .Failures() .ToImmutableArray(); + AssertCondition(!string.IsNullOrEmpty(Name), $"{nameof(Name)} is null or empty"); + HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); Hash = CalculateHash(); var expectedHash = rootElement.GetAttributeString("expectedhash", ""); if (HashMismatches(expectedHash)) { - DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})"); + FatalLoadErrors = FatalLoadErrors.Add( + new LoadError( + Message: $"Hash calculation returned {Hash.StringRepresentation}, expected {expectedHash}", + Exception: null + )); } + + FatalLoadErrors = FatalLoadErrors + .Concat(errorCatcher.Errors.Select(err => new LoadError(err.Text, null))) + .ToImmutableArray(); } public bool HashMismatches(string expectedHash) @@ -122,21 +161,21 @@ namespace Barotrauma public bool NameMatches(string name) => NameMatches(name.ToIdentifier()); - public static ContentPackage? TryLoad(string path) + public static Result TryLoad(string path) { + var (success, failure) = Result.GetFactoryMethods(); + XDocument doc = XMLExtensions.TryLoadXml(path); try { - return doc.Root.GetAttributeBool("corepackage", false) - ? (ContentPackage)new CorePackage(doc, path) - : new RegularPackage(doc, path); + return success(doc.Root.GetAttributeBool("corepackage", false) + ? new CorePackage(doc, path) + : new RegularPackage(doc, path)); } catch (Exception e) { - e = e.GetInnermost(); - DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); - return null; + return failure(e.GetInnermost()); } } @@ -181,7 +220,7 @@ namespace Barotrauma { if (!condition) { - throw new InvalidOperationException($"Failed to load \"{Name}\" at {Path}: {errorMsg}"); + FatalLoadErrors = FatalLoadErrors.Add(new LoadError(errorMsg, null)); } } @@ -201,17 +240,19 @@ namespace Barotrauma Failure } - public LoadResult LoadPackage() + public LoadResult LoadContent() { - foreach (var p in LoadPackageEnumerable()) + foreach (var p in LoadContentEnumerable()) { - if (p.Exception != null) { return LoadResult.Failure; } + if (p.Result.IsFailure) { return LoadResult.Failure; } } return LoadResult.Success; } - public IEnumerable LoadPackageEnumerable() + public IEnumerable LoadContentEnumerable() { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + ContentFile[] getFilesToLoad(Predicate predicate) => Files.Where(predicate.Invoke).ToArray() #if DEBUG @@ -227,6 +268,7 @@ namespace Barotrauma for (int i = 0; i < filesToLoad.Length; i++) { Exception? exception = null; + try { //do not allow exceptions thrown here to crash the game @@ -234,42 +276,53 @@ namespace Barotrauma } catch (Exception e) { + var innermost = e.GetInnermost(); + DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}"); exception = e; } if (exception != null) { yield return ContentPackageManager.LoadProgress.Failure(exception); - break; + yield break; } - yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length); + + if (errorCatcher.Errors.Any()) + { + yield return ContentPackageManager.LoadProgress.Failure( + ContentPackageManager.LoadProgress.Error + .Reason.ConsoleErrorsThrown); + yield break; + } + yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length); } } //Load the UI and text files first. This is to allow the game //to render the text in the loading screen as soon as possible. - var priorityFiles = getFilesToLoad(f => f is UIStyleFile || f is TextFile); + var priorityFiles = getFilesToLoad(f => f is UIStyleFile or TextFile); var remainder = getFilesToLoad(f => !priorityFiles.Contains(f)); var loadEnumerable = loadFiles(priorityFiles, 0) .Concat(loadFiles(remainder, priorityFiles.Length)); - + foreach (var p in loadEnumerable) { - if (p.Exception != null) + if (p.Result.TryUnwrapFailure(out var failure)) { - HandleLoadException(p.Exception); + errorCatcher.Dispose(); + UnloadContent(); + EnableError = Option.Some(failure); yield return p; - break; + yield break; } yield return p; } + errorCatcher.Dispose(); } - protected abstract void HandleLoadException(Exception e); - - public void UnloadPackage() + public void UnloadContent() { Files.ForEach(f => f.UnloadFile()); } @@ -284,21 +337,16 @@ namespace Barotrauma .Select(e => ContentFile.CreateFromXElement(this, e)) .ToArray(); - foreach (var result in fileResults) + foreach (var file in fileResults.Successes()) { - switch (result) + if (file is BaseSubFile or ItemAssemblyFile) { - case Success { Value: var file }: - if (file is BaseSubFile || file is ItemAssemblyFile) - { - newFileList.Add(file); - } - else - { - var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); - newFileList.Add(existingFile ?? file); - } - break; + newFileList.Add(file); + } + else + { + var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); + newFileList.Add(existingFile ?? file); } } @@ -331,16 +379,16 @@ namespace Barotrauma public void LogErrors() { - if (!Errors.Any()) + if (!FatalLoadErrors.Any()) { return; } DebugConsole.AddWarning( $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + - string.Join('\n', Errors.Select(errorToStr))); + string.Join('\n', FatalLoadErrors.Select(errorToStr))); - static string errorToStr(ContentFile.LoadError error) + static string errorToStr(LoadError error) => error.ToString(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs index 02a6f4401..123e01943 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -43,10 +43,5 @@ namespace Barotrauma "Core package requires at least one of the following content types: " + string.Join(", ", missingFileTypes.Select(t => t.Type.Name))); } - - protected override void HandleLoadException(Exception e) - { - throw new Exception($"An exception was thrown while loading \"{Name}\"", e); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs index b66bb5c10..aa77dcd35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs @@ -1,4 +1,3 @@ -using System; using System.Xml.Linq; namespace Barotrauma @@ -9,11 +8,5 @@ namespace Barotrauma { AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package"); } - - protected override void HandleLoadException(Exception e) - { - UnloadPackage(); - DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 3cb708a6c..cc29b8223 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -47,18 +47,19 @@ namespace Barotrauma { var oldCore = Core; if (newCore == oldCore) { yield break; } - Core?.UnloadPackage(); + if (newCore.FatalLoadErrors.Any()) { yield break; } + Core?.UnloadContent(); Core = newCore; - foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } + foreach (var p in newCore.LoadContentEnumerable()) { yield return p; } SortContent(); - yield return new LoadProgress(1.0f); + yield return LoadProgress.Progress(1.0f); } public static void ReloadCore() { if (Core == null) { return; } - Core.UnloadPackage(); - Core.LoadPackage(); + Core.UnloadContent(); + Core.LoadContent(); SortContent(); } @@ -79,10 +80,14 @@ namespace Barotrauma if (ReferenceEquals(inNewRegular, regular)) { yield break; } if (inNewRegular.SequenceEqual(regular)) { yield break; } ThrowIfDuplicates(inNewRegular); - var newRegular = inNewRegular.ToList(); + var newRegular = inNewRegular + // Refuse to enable packages with load errors + // so people are forced away from broken mods + .Where(r => !r.FatalLoadErrors.Any()) + .ToList(); IEnumerable toUnload = regular.Where(r => !newRegular.Contains(r)); RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray(); - toUnload.ForEach(r => r.UnloadPackage()); + toUnload.ForEach(r => r.UnloadContent()); Range loadingRange = new Range(0.0f, 1.0f); @@ -90,9 +95,9 @@ namespace Barotrauma { var package = toLoad[i]; loadingRange = new Range(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length); - foreach (var progress in package.LoadPackageEnumerable()) + foreach (var progress in package.LoadContentEnumerable()) { - if (progress.Exception != null) + if (progress.Result.IsFailure) { //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages newRegular.Remove(package); @@ -103,7 +108,7 @@ namespace Barotrauma } regular.Clear(); regular.AddRange(newRegular); SortContent(); - yield return new LoadProgress(1.0f); + yield return LoadProgress.Progress(1.0f); } public static void ThrowIfDuplicates(IEnumerable pkgs) @@ -231,10 +236,12 @@ namespace Barotrauma public sealed partial class PackageSource : ICollection { private readonly Predicate? skipPredicate; + private readonly Action? onLoadFail; - public PackageSource(string dir, Predicate? skipPredicate) + public PackageSource(string dir, Predicate? skipPredicate, Action? onLoadFail) { this.skipPredicate = skipPredicate; + this.onLoadFail = onLoadFail; directory = dir; Directory.CreateDirectory(directory); } @@ -278,25 +285,30 @@ namespace Barotrauma { var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform(); if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; } - if (File.Exists(fileListPath)) - { - if (skipPredicate?.Invoke(fileListPath) is true) { continue; } - - ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath); - if (newPackage is CorePackage corePackage) - { - corePackages.Add(corePackage); - } - else if (newPackage is RegularPackage regularPackage) - { - regularPackages.Add(regularPackage); - } - if (!(newPackage is null)) - { - Debug.WriteLine($"Loaded \"{newPackage.Name}\""); - } + if (!File.Exists(fileListPath)) { continue; } + if (skipPredicate?.Invoke(fileListPath) is true) { continue; } + + var result = ContentPackage.TryLoad(fileListPath); + if (!result.TryUnwrapSuccess(out var newPackage)) + { + onLoadFail?.Invoke( + fileListPath, + result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable")); + continue; } + + switch (newPackage) + { + case CorePackage corePackage: + corePackages.Add(corePackage); + break; + case RegularPackage regularPackage: + regularPackages.Add(regularPackage); + break; + } + + Debug.WriteLine($"Loaded \"{newPackage.Name}\""); } } @@ -348,8 +360,20 @@ namespace Barotrauma public bool IsReadOnly => true; } - public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null); - public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath); + public static readonly PackageSource LocalPackages + = new PackageSource( + ContentPackage.LocalModsDir, + skipPredicate: null, + onLoadFail: null); + public static readonly PackageSource WorkshopPackages = new PackageSource( + ContentPackage.WorkshopModsDir, + skipPredicate: SteamManager.Workshop.IsInstallingToPath, + onLoadFail: (fileListPath, exception) => + { + // Delete Workshop mods that fail to load to + // force a reinstall on next launch if necessary + Directory.TryDelete(Path.GetDirectoryName(fileListPath)!); + }); public static CorePackage? VanillaCorePackage { get; private set; } = null; @@ -373,63 +397,77 @@ namespace Barotrauma EnabledPackages.DisableRemovedMods(); } - public static ContentPackage? ReloadContentPackage(ContentPackage p) + public static Result ReloadContentPackage(ContentPackage p) { - ContentPackage? newPackage = ContentPackage.TryLoad(p.Path); - if (newPackage is CorePackage core) - { - if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } - } - else if (newPackage is RegularPackage regular) - { - int index = EnabledPackages.Regular.IndexOf(p); - if (index >= 0) - { - var newRegular = EnabledPackages.Regular.ToArray(); - newRegular[index] = regular; - EnabledPackages.SetRegular(newRegular); - } - } + var result = ContentPackage.TryLoad(p.Path); - if (newPackage != null) + if (result.TryUnwrapSuccess(out var newPackage)) { + switch (newPackage) + { + case CorePackage core: + { + if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } + + break; + } + case RegularPackage regular: + { + int index = EnabledPackages.Regular.IndexOf(p); + if (index >= 0) + { + var newRegular = EnabledPackages.Regular.ToArray(); + newRegular[index] = regular; + EnabledPackages.SetRegular(newRegular); + } + + break; + } + } + LocalPackages.SwapPackage(p, newPackage); WorkshopPackages.SwapPackage(p, newPackage); } EnabledPackages.DisableRemovedMods(); - return newPackage; + return result; } - public readonly struct LoadProgress + public readonly record struct LoadProgress(Result Result) { - public readonly float Value; - public readonly Exception? Exception; - - public LoadProgress(float value) + public readonly record struct Error( + Error.Reason ErrorReason, + Option Exception) { - Value = value; - Exception = null; - } + public enum Reason { Exception, ConsoleErrorsThrown } - private LoadProgress(Exception exception) - { - Value = -1f; - Exception = exception; + public Error(Reason reason) : this(reason, Option.None) { } + public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { } } public static LoadProgress Failure(Exception exception) - => new LoadProgress(exception); + => new LoadProgress( + Result.Failure(new Error(exception))); + + public static LoadProgress Failure(Error.Reason reason) + => new LoadProgress( + Result.Failure(new Error(reason))); + + public static LoadProgress Progress(float value) + => new LoadProgress( + Result.Success(value)); public LoadProgress Transform(Range range) - => Exception != null - ? this - : new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value)); + => Result.TryUnwrapSuccess(out var value) + ? new LoadProgress( + Result.Success( + MathHelper.Lerp(range.Start, range.End, value))) + : this; } public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); - foreach (ContentFile.LoadError error in VanillaCorePackage.Errors) + foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors) { DebugConsole.ThrowError(error.ToString()); } @@ -444,6 +482,8 @@ namespace Barotrauma if (VanillaCorePackage is null) { LoadVanillaFileList(); } + SteamManager.Workshop.DeleteUnsubscribedMods(); + CorePackage enabledCorePackage = VanillaCorePackage!; List enabledRegularPackages = new List(); @@ -512,7 +552,7 @@ namespace Barotrauma yield return p.Transform(loadingRange); } - yield return new LoadProgress(1.0f); + yield return LoadProgress.Progress(1.0f); } public static void LogEnabledRegularPackageErrors() diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index b184da625..f2b9dbae6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -1,6 +1,8 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.Metadata.Ecma335; using System.Xml.Linq; @@ -63,6 +65,8 @@ namespace Barotrauma public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + [return:NotNullIfNotNull("def")] + public ImmutableHashSet? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs index da8f9ac27..2e8327f55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs @@ -126,6 +126,10 @@ namespace Barotrauma { return new Identifier(str); } + internal int IndexOf(char c) => Value.IndexOf(c); + + internal Identifier this[Range range] => Value[range].ToIdentifier(); + internal Char this[int i] => Value[i]; } public static class IdentifierExtensions diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 56fce24c8..d0a94abe9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -5,6 +5,7 @@ using Barotrauma.Steam; using FarseerPhysics; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -15,12 +16,12 @@ using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { - struct ColoredText + readonly struct ColoredText { - public string Text; - public Color Color; - public bool IsCommand; - public bool IsError; + public readonly string Text; + public readonly Color Color; + public readonly bool IsCommand; + public readonly bool IsError; public readonly string Time; @@ -31,7 +32,7 @@ namespace Barotrauma this.IsCommand = isCommand; this.IsError = isError; - Time = DateTime.Now.ToString(); + Time = DateTime.Now.ToString(CultureInfo.InvariantCulture); } } @@ -72,7 +73,7 @@ namespace Barotrauma bool allowCheats = false; #if CLIENT - allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); + allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true }); #endif if (!allowCheats && !CheatsEnabled && IsCheat) { @@ -92,13 +93,57 @@ namespace Barotrauma } } - private static readonly Queue queuedMessages = new Queue(); + private static readonly ConcurrentQueue queuedMessages + = new ConcurrentQueue(); + public static readonly NamedEvent MessageHandler = new NamedEvent(); + + public struct ErrorCatcher : IDisposable + { + private readonly List errors; + private readonly bool wasConsoleOpen; + private Identifier handlerId; + public IReadOnlyList Errors => errors; + + private ErrorCatcher(Identifier handlerId) + { + this.handlerId = handlerId; +#if CLIENT + this.wasConsoleOpen = IsOpen; +#else + this.wasConsoleOpen = false; +#endif + this.errors = new List(); + + //create a local variable that can be captured by lambdas + var errs = this.errors; + + MessageHandler.Register(handlerId, msg => + { + if (!msg.IsError) { return; } + errs.Add(msg); + }); + } + + public static ErrorCatcher Create() + => new ErrorCatcher(ToolBox.RandomSeed(25).ToIdentifier()); + + public void Dispose() + { + if (handlerId.IsEmpty) { return; } + MessageHandler.Deregister(handlerId); + handlerId = Identifier.Empty; +#if CLIENT + DebugConsole.IsOpen = wasConsoleOpen; +#endif + } + } + static partial void ShowHelpMessage(Command command); const int MaxMessages = 300; - public static List Messages = new List(); + public static readonly List Messages = new List(); public delegate void QuestionCallback(string answer); private static QuestionCallback activeQuestionCallback; @@ -1751,6 +1796,17 @@ namespace Barotrauma NewMessage("Set minimum loading time to " + time + " seconds.", Color.White); })); + + commands.Add(new Command("resetcharacternetstate", "resetcharacternetstate [character name]: A debug-only command that resets a character's network state, intended for diagnosing character syncing issues.", null, + () => + { + if (GameMain.NetworkMember == null) { return null; } + return new string[][] + { + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() + }; + })); + commands.Add(new Command("storeinfo", "", (string[] args) => { if (GameMain.GameSession?.Map?.CurrentLocation is Location location) @@ -1804,6 +1860,7 @@ namespace Barotrauma commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false)); commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); @@ -2188,7 +2245,7 @@ namespace Barotrauma //Dont do a thing, random is basically Human points anyways - its in the help description. break; default: - var matchingCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray()); if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; } break; } @@ -2276,11 +2333,10 @@ namespace Barotrauma private static void NewMessage(string msg, Color color, bool isCommand, bool isError) { if (string.IsNullOrEmpty(msg)) { return; } - - lock (queuedMessages) - { - queuedMessages.Enqueue(new ColoredText(msg, color, isCommand, isError)); - } + + var newMsg = new ColoredText(msg, color, isCommand, isError); + queuedMessages.Enqueue(newMsg); + MessageHandler.Invoke(newMsg); } public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index bf8d1addd..baa0aecd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -43,6 +43,7 @@ namespace Barotrauma OnRepairComplete, OnItemFabricationSkillGain, OnItemFabricatedAmount, + OnItemFabricatedIngredients, OnAllyItemFabricatedAmount, OnOpenItemContainer, OnUseRangedWeapon, @@ -51,6 +52,7 @@ namespace Barotrauma OnSelfRagdoll, OnRagdoll, OnRoundEnd, + OnLootCharacter, OnAnyMissionCompleted, OnAllMissionsCompleted, OnGiveOrder, @@ -80,6 +82,11 @@ namespace Barotrauma // Skills ElectricalSkillBonus, HelmSkillBonus, + HelmSkillOverride, + MedicalSkillOverride, + WeaponsSkillOverride, + ElectricalSkillOverride, + MechanicalSkillOverride, MechanicalSkillBonus, MedicalSkillBonus, WeaponsSkillBonus, @@ -105,6 +112,7 @@ namespace Barotrauma RangedSpreadReduction, // Utility RepairSpeed, + MechanicalRepairSpeed, DeconstructorSpeedMultiplier, RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, @@ -115,20 +123,54 @@ namespace Barotrauma GeneticMaterialRefineBonus, GeneticMaterialTaintedProbabilityReductionOnCombine, SkillGainSpeed, + ExtraLevelGain, + HelmSkillGainSpeed, + WeaponsSkillGainSpeed, + MedicalSkillGainSpeed, + ElectricalSkillGainSpeed, + MechanicalSkillGainSpeed, MedicalItemApplyingMultiplier, + MedicalItemDurationMultiplier, + PoisonMultiplier, // Tinker TinkeringDuration, TinkeringStrength, TinkeringDamage, // Misc ReputationGainMultiplier, + ReputationLossMultiplier, MissionMoneyGainMultiplier, ExperienceGainMultiplier, MissionExperienceGainMultiplier, ExtraMissionCount, ExtraSpecialSalesCount, - ApplyTreatmentsOnSelfFraction, + StoreSellMultiplier, + StoreBuyMultiplierAffiliated, + StoreBuyMultiplier, MaxAttachableCount, + ExplosionRadiusMultiplier, + ExplosionDamageMultiplier, + FabricationSpeed, + BallastFloraDamageMultiplier, + HoldBreathMultiplier, + Apprenticeship, + Affiliation, + CPRBoost + } + + internal enum ItemTalentStats + { + None, + DetoriationSpeed, + BatteryCapacity, + EngineSpeed, + EngineMaxSpeed, + PumpSpeed, + PumpMaxFlow, + ReactorMaxOutput, + ReactorFuelEfficiency, + DeconstructorSpeed, + FabricationSpeed } [Flags] @@ -145,8 +187,8 @@ namespace Barotrauma GainSkillPastMaximum = 0x80, RetainExperienceForNewCharacter = 0x100, AllowSecondOrderedTarget = 0x200, - PowerfulCPR = 0x400, - AlwaysStayConscious = 0x800, + AlwaysStayConscious = 0x400, + CanNotDieToAfflictions = 0x800, } [Flags] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 9413193d1..e84bd7676 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -25,6 +26,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool RequireEquipped { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool Recursive { get; set; } + [Serialize(-1, IsPropertySaveable.Yes)] public int ItemContainerIndex { get; set; } @@ -50,6 +54,11 @@ namespace Barotrauma break; } conditionals = conditionalList; + + if (itemTags.None() && ItemIdentifiers.None()) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check."); + } } protected override bool? DetermineSuccess() @@ -97,7 +106,22 @@ namespace Barotrauma { if (inventory == null) { return false; } int count = 0; - foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier))) + HashSet eventTargets = new HashSet(); + foreach (Identifier tag in itemTags) + { + foreach (var target in ParentEvent.GetTargets(tag)) + { + if (target is Item item) + { + eventTargets.Add(item); + } + } + } + foreach (Item item in inventory.FindAllItems(it => + itemTags.Any(it.HasTag) || + itemIdentifierSplit.Contains(it.Prefab.Identifier) || + eventTargets.Contains(it), + recursive: Recursive)) { if (!ConditionalsMatch(item, character)) { continue; } count++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs new file mode 100644 index 000000000..530a63429 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; + +namespace Barotrauma; + +class CheckMissionAction : BinaryOptionAction +{ + public enum MissionType + { + Current, + Selected, + Available + } + + [Serialize(MissionType.Current, IsPropertySaveable.Yes)] + public MissionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MissionCount { get; set; } + + public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + MissionCount = Math.Max(MissionCount, 0); + } + + protected override bool? DetermineSuccess() + { + var missions = Type switch + { + MissionType.Current => GameMain.GameSession?.Missions, + MissionType.Selected => GameMain.GameSession?.Campaign?.Missions, + MissionType.Available => GameMain.GameSession?.Map?.CurrentLocation?.AvailableMissions, + _ => null + }; + if (missions is not null) + { + if (!MissionIdentifier.IsEmpty) + { + return missions.Any(m => m.Prefab.Identifier == MissionIdentifier); + } + else if (!MissionTag.IsEmpty) + { + return missions.Count(m => m.Prefab.Tags.Contains(MissionTag.Value)) >= MissionCount; + } + else + { + return missions.Count() >= MissionCount; + } + } + return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionCount == 0; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs new file mode 100644 index 000000000..15e5e90a9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs @@ -0,0 +1,15 @@ +namespace Barotrauma; + +partial class CheckObjectiveAction : BinaryOptionAction +{ + public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + bool success = false; + DetermineSuccessProjSpecific(ref success); + return success; + } + + partial void DetermineSuccessProjSpecific(ref bool success); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 6723d3abb..870a7ee9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -1,7 +1,15 @@ +using Barotrauma.Extensions; + namespace Barotrauma { class CheckOrderAction : BinaryOptionAction { + public enum OrderPriority + { + Top, + Any + } + [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -14,35 +22,58 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier OrderTargetTag { get; set; } + [Serialize(OrderPriority.Any, IsPropertySaveable.Yes)] + public OrderPriority Priority { get; set; } + public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { - Character targetCharacter = null; - if (!TargetTag.IsEmpty) + var targetCharacters = ParentEvent.GetTargets(TargetTag); + if (targetCharacters.None()) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) - { - if (t is Character c) - { - targetCharacter = c; - break; - } - } - } - if (targetCharacter == null) - { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); return false; } - var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Identifier == OrderIdentifier) + foreach (var t in targetCharacters) { - if (!OrderTargetTag.IsEmpty) + if (t is not Character c) { - if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; } + continue; + } + if (Priority == OrderPriority.Top) + { + if (c.GetCurrentOrderWithTopPriority() is Order topPrioOrder && IsMatch(topPrioOrder)) + { + return true; + } + } + else if (Priority == OrderPriority.Any) + { + foreach (var order in c.CurrentOrders) + { + if (IsMatch(order)) + { + return true; + } + } + } + + bool IsMatch(Order order) + { + if (order?.Identifier == OrderIdentifier) + { + if (!OrderTargetTag.IsEmpty && (order.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag))) + { + return false; + } + if (OrderOption.IsEmpty || order?.Option == OrderOption) + { + return true; + } + } + return false; } - return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs new file mode 100644 index 000000000..b0ac35616 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; + +namespace Barotrauma; + +class CheckPurchasedItemsAction : BinaryOptionAction +{ + public enum TransactionType + { + Purchased, + Sold + } + + [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)] + public TransactionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MinCount { get; set; } + + public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + MinCount = Math.Max(MinCount, 1); + } + + protected override bool? DetermineSuccess() + { + if (ItemIdentifier.IsEmpty && ItemTag.IsEmpty) + { + return false; + } + if (GameMain.GameSession?.Campaign?.CargoManager is not CargoManager cargoManager) + { + return false; + } + if (Type == TransactionType.Purchased) + { + int totalPurchased = 0; + foreach ((Identifier id, var items) in cargoManager.PurchasedItems) + { + if (!ItemIdentifier.IsEmpty) + { + totalPurchased += items.Find(i => i.ItemPrefabIdentifier == ItemIdentifier)?.Quantity ?? 0; + } + else if (!ItemTag.IsEmpty) + { + foreach (var item in items) + { + if (item.ItemPrefab.Tags.Contains(ItemTag)) + { + totalPurchased += item.Quantity; + } + } + } + if (totalPurchased >= MinCount) + { + return true; + } + } + } + else + { + int totalSold = 0; + foreach ((Identifier id, var items) in cargoManager.SoldItems) + { + if (!ItemIdentifier.IsEmpty) + { + totalSold += items.Count(i => i.ItemPrefab.Identifier == ItemIdentifier); + } + else if (!ItemTag.IsEmpty) + { + totalSold += items.Count(i => i.ItemPrefab.Tags.Contains(ItemTag)); + } + if (totalSold >= MinCount) + { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index a438db9a5..1c5c8f0b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -59,7 +59,10 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } - public Character speaker + [Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreInterruptDistance { get; set; } + + public Character Speaker { get; private set; @@ -124,7 +127,7 @@ namespace Barotrauma #else foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); } + if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); } } #endif ResetSpeaker(); @@ -160,7 +163,7 @@ namespace Barotrauma selectedOption = -1; interrupt = false; dialogOpened = false; - speaker = null; + Speaker = null; } public override bool SetGoToTarget(string goTo) @@ -181,15 +184,14 @@ namespace Barotrauma private void ResetSpeaker() { - if (speaker == null) { return; } - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.ActiveConversation = null; - speaker.SetCustomInteract(null, null); + if (Speaker == null) { return; } + Speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + Speaker.ActiveConversation = null; + Speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); + GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData()); #endif - var humanAI = speaker.AIController as HumanAIController; - if (humanAI != null && !speaker.IsDead && !speaker.Removed) + if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed) { humanAI.ClearForcedOrder(); if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } @@ -207,7 +209,6 @@ namespace Barotrauma public override void Update(float deltaTime) { - lastActiveTime = Timing.TotalTime; if (interrupt) { Interrupted?.Update(deltaTime); @@ -216,6 +217,7 @@ namespace Barotrauma { if (dialogOpened) { + lastActiveTime = Timing.TotalTime; #if CLIENT if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) { @@ -226,7 +228,7 @@ namespace Barotrauma Reset(); } #endif - if (ShouldInterrupt()) + if (ShouldInterrupt(requireTarget: true)) { ResetSpeaker(); interrupt = true; @@ -236,34 +238,34 @@ namespace Barotrauma if (!SpeakerTag.IsEmpty) { - if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } - speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; - if (speaker == null || speaker.Removed) + if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } + Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; + if (Speaker == null || Speaker.Removed) { return; } //some conversation already assigned to the speaker, wait for it to be removed - if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) + if (Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } else if (!WaitForInteraction) { - TryStartConversation(speaker); + TryStartConversation(Speaker); } - else if (speaker.ActiveConversation != this) + else if (Speaker.ActiveConversation != this) { - speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; - speaker.ActiveConversation = this; + Speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; + Speaker.ActiveConversation = this; #if CLIENT - speaker.SetCustomInteract( + Speaker.SetCustomInteract( TryStartConversation, TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else - speaker.SetCustomInteract( + Speaker.SetCustomInteract( TryStartConversation, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); + GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData()); #endif } return; @@ -275,7 +277,9 @@ namespace Barotrauma } else { - if (ShouldInterrupt()) + //after the conversation has been finished and the target character assigned, + //we no longer care if we still have a target + if (ShouldInterrupt(requireTarget: false)) { ResetSpeaker(); interrupt = true; @@ -287,35 +291,36 @@ namespace Barotrauma } } - private bool ShouldInterrupt() + private bool ShouldInterrupt(bool requireTarget) { IEnumerable targets = Enumerable.Empty(); - if (!TargetTag.IsEmpty) + if (!TargetTag.IsEmpty && requireTarget) { - targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); + targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget)); if (!targets.Any()) { return true; } } - if (speaker != null) + if (Speaker != null) { - if (!TargetTag.IsEmpty) + if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance) { - if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } + if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } } - if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction()) + if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction()) { return true; } - return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated; + return Speaker.Removed || Speaker.IsDead || Speaker.IsIncapacitated; } return false; } - private bool IsValidTarget(Entity e) + private bool IsValidTarget(Entity e, bool requirePlayerControlled = true) { - bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && - (e == Character.Controlled || character.IsRemotePlayer); + bool isValid = + e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && + (character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled); #if SERVER if (!dialogOpened) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index c0fc93a5b..b570a750d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -40,7 +40,8 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction."); continue; } - Actions.Add(Instantiate(scriptedEvent, e)); + var action = Instantiate(scriptedEvent, e); + if (action != null) { Actions.Add(action); } } } @@ -149,6 +150,10 @@ namespace Barotrauma ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) }); try { + if (constructor == null) + { + throw new Exception($"Error in scripted event \"{scriptedEvent.Prefab.Identifier}\" - could not find a constructor for the EventAction \"{actionType}\"."); + } return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction; } catch (Exception ex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs index 2834b8b84..6ac082e66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -49,6 +49,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier ObjectiveTag { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool ObjectiveCanBeCompleted { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ParentObjectiveId { get; set; } + private bool isFinished = false; public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 143563584..ee26b9283 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -21,7 +22,14 @@ namespace Barotrauma private bool isFinished = false; - public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); + if (!enums.Contains(TeamTag)) + { + DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); + } + } private List affectedNpcs = null; @@ -59,7 +67,7 @@ namespace Barotrauma void ChangeItemTeam(Submarine sub, bool allowStealing) { - foreach (Item item in npc.Inventory.AllItems) + foreach (Item item in npc.Inventory.FindAllItems(recursive: true)) { item.AllowStealing = allowStealing; if (item.GetComponent() is { } wifiComponent) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index a3aa06d7a..ea2851339 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -1,9 +1,8 @@ -using System; -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -11,6 +10,7 @@ namespace Barotrauma { public enum SpawnLocationType { + Any, MainSub, Outpost, MainPath, @@ -40,7 +40,7 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)] + [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)] public SpawnLocationType SpawnLocation { get; set; } [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] @@ -177,7 +177,7 @@ namespace Barotrauma } else if (!ItemIdentifier.IsEmpty) { - if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab)) + if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); } @@ -257,21 +257,11 @@ namespace Barotrauma { if (!SpawnPointTag.IsEmpty) { - List potentialItems = SpawnLocation switch - { - SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub), - SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null), - SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin), - SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon), - _ => throw new NotImplementedException() - }; - + List potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced(); + var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced(); if (target != null) { return target; } } @@ -281,19 +271,26 @@ namespace Barotrauma return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); } + private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine) + { + return spawnLocation switch + { + SpawnLocationType.Any => true, + SpawnLocationType.MainSub => submarine == Submarine.MainSub, + SpawnLocationType.MainPath => submarine == null, + SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } }, + SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } }, + SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } }, + SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null, + _ => throw new NotImplementedException(), + }; + } + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { - List potentialSpawnPoints = spawnLocation switch - { - SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null), - SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null), - SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin), - SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon), - _ => throw new NotImplementedException() - }; - + bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; + List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); + potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); if (moduleFlags != null && moduleFlags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 208a7fbfa..89eff4f39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -32,11 +32,13 @@ namespace Barotrauma ("bot", v => TagBots(playerCrewOnly: false)), ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), + ("jobidentifier", TagHumansByJobIdentifier), ("structureidentifier", TagStructuresByIdentifier), ("structurespecialtag", TagStructuresBySpecialTag), ("itemidentifier", TagItemsByIdentifier), ("itemtag", TagItemsByTag), - ("hullname", TagHullsByName) + ("hullname", TagHullsByName), + ("submarine", TagSubmarinesByType), }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); } @@ -93,6 +95,18 @@ namespace Barotrauma } } } + + private void TagHumansByJobIdentifier(Identifier jobIdentifier) + { + foreach (Character c in Character.CharacterList) + { + if (c.HasJob(jobIdentifier)) + { + ParentEvent.AddTarget(Tag, c); + } + } + } + private void TagStructuresByIdentifier(Identifier identifier) { ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); @@ -118,6 +132,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } + private void TagSubmarinesByType(Identifier type) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 385dc13ae..8f7a849ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -8,6 +8,12 @@ namespace Barotrauma { class TriggerAction : EventAction { + public enum TriggerType + { + Inside, + Outside + } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")] public Identifier Target1Tag { get; set; } @@ -23,7 +29,10 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")] public Identifier ApplyToTarget2 { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")] + [Serialize(TriggerType.Inside, IsPropertySaveable.Yes, description: "Determines if the targets must be inside or outside of the radius.")] + public TriggerType Type { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range to activate the trigger.")] public float Radius { get; set; } [Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] @@ -38,6 +47,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] public bool AllowMultipleTargets { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")] + public bool CheckAllTargets { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -57,6 +69,8 @@ namespace Barotrauma public bool isRunning = false; private readonly List> npcsOrItems = new List>(); + + private readonly List<(Entity e1, Entity e2)> triggerers = new List<(Entity e1, Entity e2)>(); public override void Update(float deltaTime) { @@ -66,18 +80,44 @@ namespace Barotrauma var targets1 = ParentEvent.GetTargets(Target1Tag); if (!targets1.Any()) { return; } - + + triggerers.Clear(); foreach (Entity e1 in targets1) { - if (DisableInCombat && IsInCombat(e1)) { continue; } - if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; } + if (DisableInCombat && IsInCombat(e1)) + { + if (CheckAllTargets) + { + return; + } + continue; + } + if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) + { + if (CheckAllTargets) + { + return; + } + continue; + } if (!TargetModuleType.IsEmpty) { - if (IsCloseEnoughToHull(e1, out Hull hull)) + if (!CheckAllTargets && CheckDistanceToHull(e1, out Hull hull)) { Trigger(e1, hull); return; } + else if (CheckAllTargets) + { + if (CheckDistanceToHull(e1, out hull)) + { + triggerers.Add((e1, hull)); + } + else + { + return; + } + } continue; } @@ -85,9 +125,26 @@ namespace Barotrauma foreach (Entity e2 in targets2) { - if (e1 == e2) { continue; } - if (DisableInCombat && IsInCombat(e2)) { continue; } - if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; } + if (e1 == e2) + { + continue; + } + if (DisableInCombat && IsInCombat(e2)) + { + if (CheckAllTargets) + { + return; + } + continue; + } + if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) + { + if (CheckAllTargets) + { + return; + } + continue; + } if (WaitForInteraction) { @@ -173,16 +230,35 @@ namespace Barotrauma Vector2 pos1 = e1.WorldPosition; Vector2 pos2 = e2.WorldPosition; distance = Vector2.Distance(pos1, pos2); - if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) || - ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) || - Vector2.DistanceSquared(pos1, pos2) < Radius * Radius) + if ((Type == TriggerType.Inside) == IsWithinRadius()) + { + if (!CheckAllTargets) + { + Trigger(e1, e2); + return; + } + else + { + triggerers.Add((e1, e2)); + } + } + else if (CheckAllTargets) { - Trigger(e1, e2); return; } + + bool IsWithinRadius() => + ((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) || + ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) || + Vector2.DistanceSquared(pos1, pos2) < Radius * Radius; } } - } + } + + foreach (var (e1, e2) in triggerers) + { + Trigger(e1, e2); + } } private void ResetTargetIcons() @@ -205,7 +281,7 @@ namespace Barotrauma } } - private bool IsCloseEnoughToHull(Entity e, out Hull hull) + private bool CheckDistanceToHull(Entity e, out Hull hull) { hull = null; if (Radius <= 0) @@ -213,36 +289,35 @@ namespace Barotrauma if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType)) { hull = character.CurrentHull; - return true; + return Type == TriggerType.Inside; } else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType)) { hull = item.CurrentHull; - return true; + return Type == TriggerType.Inside; } - return false; + return Type == TriggerType.Outside; } else { foreach (Hull potentialHull in Hull.HullList) { if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; } - Rectangle hullRect = potentialHull.WorldRect; hullRect.Inflate(Radius, Radius); if (Submarine.RectContains(hullRect, e.WorldPosition)) { hull = potentialHull; - return true; + return Type == TriggerType.Inside; } } - return false; + return Type == TriggerType.Outside; } } - private bool IsInCombat(Entity entity) + private static bool IsInCombat(Entity entity) { - if (!(entity is Character character)) { return false; } + if (entity is not Character character) { return false; } foreach (Character c in Character.CharacterList) { if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs index 587fb20a7..f2128f82e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs @@ -13,6 +13,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier ObjectiveTag { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool CanBeCompleted { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ParentObjectiveId { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool AutoPlayVideo { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs index b990fdf66..3182cfae9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -43,6 +43,9 @@ partial class UIHighlightAction : EventAction [Serialize(true, IsPropertySaveable.Yes)] public bool Bounce { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool HighlightMultiple { get; set; } + private bool isFinished; public UIHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 8ecedce89..dee7f7b0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -73,6 +73,7 @@ namespace Barotrauma private readonly HashSet finishedEvents = new HashSet(); private readonly HashSet nonRepeatableEvents = new HashSet(); + private readonly HashSet usedUniqueSets = new HashSet(); #if DEBUG && SERVER @@ -155,12 +156,19 @@ namespace Barotrauma } rand = new MTRandom(seed); - EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; + EventSet initialEventSet = SelectRandomEvents( + EventSet.Prefabs.ToList(), + requireCampaignSet: playingCampaign, + random: rand); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; - initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + initialEventSet = SelectRandomEvents( + EventSet.Prefabs.Where(e => !e.Additive).ToList(), + requireCampaignSet: playingCampaign, + random: rand); } if (initialEventSet != null) { @@ -199,24 +207,23 @@ namespace Barotrauma level.StartLocation.Connections.ForEach(c => c.Locked = false); } } - - AddChildEvents(initialEventSet); - void AddChildEvents(EventSet eventSet) + } + RegisterNonRepeatableChildEvents(initialEventSet); + void RegisterNonRepeatableChildEvents(EventSet eventSet) + { + if (eventSet == null) { return; } + if (eventSet.OncePerLevel) { - if (eventSet == null) { return; } - if (eventSet.OncePerOutpost) + foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { - foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) - { - nonRepeatableEvents.Add(ep); - } - } - foreach (EventSet childSet in eventSet.ChildSets) - { - AddChildEvents(childSet); + nonRepeatableEvents.Add(ep); } } - } + foreach (EventSet childSet in eventSet.ChildSets) + { + RegisterNonRepeatableChildEvents(childSet); + } + } } PreloadContent(GetFilesToPreload()); @@ -351,6 +358,7 @@ namespace Barotrauma QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); + usedUniqueSets.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); @@ -364,16 +372,18 @@ namespace Barotrauma /// public void RegisterEventHistory() { + if (level?.LevelData == null) { return; } + level.LevelData.EventsExhausted = true; - if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) + if (level.LevelData.Type == LevelData.LevelType.Outpost) { level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); if (level.LevelData.EventHistory.Count > MaxEventHistory) { level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); } + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); } public void SkipEventCooldown() @@ -402,7 +412,7 @@ namespace Barotrauma List> spawnPosFilter = new List>(); if (eventSet.PerRuin) { - applyCount = level.Ruins.Count(); + applyCount = level.Ruins.Count; foreach (var ruin in level.Ruins) { spawnPosFilter.Add(pos => pos.Ruin == ruin); @@ -410,7 +420,7 @@ namespace Barotrauma } else if (eventSet.PerCave) { - applyCount = level.Caves.Count(); + applyCount = level.Caves.Count; foreach (var cave in level.Caves) { spawnPosFilter.Add(pos => pos.Cave == cave); @@ -427,8 +437,8 @@ namespace Barotrauma } bool isPrefabSuitable(EventPrefab e) - => e.BiomeIdentifier.IsEmpty || - e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; + => (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + !level.LevelData.NonRepeatableEvents.Contains(e); foreach (var subEventPrefab in eventSet.EventPrefabs) { @@ -496,12 +506,12 @@ namespace Barotrauma selectedEvents[eventSet].Add(newEvent); } - Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; + var location = GetEventLocation(); foreach (EventSet childEventSet in eventSet.ChildSets) { if (!IsValidForLevel(childEventSet, level)) { continue; } - if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } - CreateEvents(childEventSet); + if (!IsValidForLocation(childEventSet, location)) { continue; } + CreateEvents(childEventSet); } } } @@ -536,10 +546,32 @@ namespace Barotrauma } } - Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; - if (location != null) + var location = GetEventLocation(); + allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); + + allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly || + (GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true })); + + int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location); + int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location); + if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex)) { - allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); + allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex); + } + else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex)) + { + allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex); + } + else + { + // When there are no forced sets, only allow sets that aren't forced at any specific location + allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0); + } + + if (allowedEventSets.Count() == 1) + { + // When there's only a single set available, just select it directly + return allowedEventSets.First(); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -558,7 +590,7 @@ namespace Barotrauma return null; } - private bool IsValidForLevel(EventSet eventSet, Level level) + private static bool IsValidForLevel(EventSet eventSet, Level level) { return level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && @@ -568,8 +600,16 @@ namespace Barotrauma private bool IsValidForLocation(EventSet eventSet, Location location) { - return eventSet.LocationTypeIdentifiers == null || - eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier); + if (location is null) { return true; } + var locationType = location.GetLocationType(); + bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents; + if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; } + return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier); + } + + private Location GetEventLocation() + { + return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation; } private bool CanStartEventSet(EventSet eventSet) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index cd6a3cae6..a5f16c350 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -113,7 +113,10 @@ namespace Barotrauma public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; - public readonly bool OncePerOutpost; + /// + /// If true, events from this set can only occur once in the level. + /// + public readonly bool OncePerLevel; public readonly bool DelayWhenCrewAway; @@ -126,6 +129,18 @@ namespace Barotrauma public readonly float ResetTime; + /// + /// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.) + /// + public readonly int ForceAtDiscoveredNr; + + /// + /// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.) + /// + public readonly int ForceAtVisitedNr; + + public readonly bool CampaignTutorialOnly; + public readonly struct SubEventPrefab { public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) @@ -268,10 +283,18 @@ namespace Barotrauma DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); - OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); + OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); ResetTime = element.GetAttributeFloat("resettime", 0); + CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); + + ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); + ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); + if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0) + { + DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior"); + } DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f); foreach (var subElement in element.Elements()) @@ -489,6 +512,11 @@ namespace Barotrauma } } + public override string ToString() + { + return $"{base.ToString()} ({Identifier.Value})"; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index f203441b6..7b162b544 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -378,7 +378,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); + var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); @@ -386,16 +386,27 @@ namespace Barotrauma #if CLIENT foreach (Character character in crewCharacters) { - character.Info?.GiveExperience(experienceGain, isMissionExperience: true); + GiveMissionExperience(character.Info); } #else foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) { //give the experience to the stored characterinfo if the client isn't currently controlling a character - (c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true); + GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo); + } + foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + { + GiveMissionExperience(bot.Info); } #endif + void GiveMissionExperience(CharacterInfo info) + { + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value)); + } + // apply money gains afterwards to prevent them from affecting XP gains var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); @@ -619,4 +630,16 @@ namespace Barotrauma public Mission Mission { get; set; } } + class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + { + public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier) + { + Value = missionExperienceGainMultiplier; + Mission = mission; + } + + public float Value { get; set; } + public Mission Mission { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 1bdbade40..d8ccd3753 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -594,7 +594,7 @@ namespace Barotrauma { GameAnalyticsManager.AddDesignEvent( $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}", - value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); + value: GameMain.GameSession.RoundDuration); } }, delayBetweenSpawns * i); i++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 9ea8ff2a0..7aa700475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -218,6 +218,8 @@ namespace Barotrauma.Extensions return new Dictionary(immutableDictionary); } + public static NetCollection ToNetCollection(this IEnumerable enumerable) => new NetCollection(enumerable.ToImmutableArray()); + /// /// Returns whether a given collection has at least a certain amount /// of elements for which the predicate returns true. @@ -308,5 +310,17 @@ namespace Barotrauma.Extensions => source .OfType>() .Select(some => some.Value); + + public static IEnumerable Successes( + this IEnumerable> source) + => source + .OfType>() + .Select(s => s.Value); + + public static IEnumerable Failures( + this IEnumerable> source) + => source + .OfType>() + .Select(f => f.Error); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 4d06bc9a9..adb68793d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -163,6 +163,7 @@ namespace Barotrauma { if (!subs.Contains(item.Submarine)) { continue; } if (item.GetRootInventoryOwner() is Character) { continue; } + if (item.NonInteractable) { continue; } containers.AddRange(item.GetComponents()); } containers.Shuffle(Rand.RandSync.ServerAndClient); @@ -305,14 +306,6 @@ namespace Barotrauma return validContainers; } - private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1] - { - (0, 1.0f), - (1, 0.0f), - (2, 0.0f), - (3, 0.0f), - }; - private static List CreateItems(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) { List newItems = new List(); @@ -335,11 +328,7 @@ namespace Barotrauma break; } var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab); - int quality = - existingItem?.Quality ?? - ToolBox.SelectWeightedRandom( - qualityCommonnesses.Select(q => q.quality).ToList(), - qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient); + int quality = existingItem?.Quality ?? Quality.GetSpawnedItemQuality(validContainer.Key.Item.Submarine, Level.Loaded, Rand.RandSync.ServerAndClient); if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 6fa21d9ae..edcf1532a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -158,6 +158,16 @@ namespace Barotrauma this.campaign = campaign; } + public static bool HasUnlockedStoreItem(ItemPrefab prefab) + { + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.HasStoreAccessForItem(prefab)) { return true; } + } + + return false; + } + private List GetItems(Identifier identifier, Dictionary> items, bool create = false) { if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 06dc62593..7f78f853d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -62,6 +62,7 @@ namespace Barotrauma if (!data.ContainsKey(identifier)) { data.Add(identifier, value); + SteamAchievementManager.OnCampaignMetadataSet(identifier, value); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 581566f00..32dc12d72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -4,6 +4,12 @@ using System; namespace Barotrauma { + public enum FactionAffiliation + { + Affiliated, + Neutral + } + class Faction { public Reputation Reputation { get; } @@ -14,6 +20,27 @@ namespace Barotrauma Prefab = prefab; Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); } + + /// + /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents + /// + /// + public FactionAffiliation GetPlayerAffiliationStatus() + { + float affiliation = 1f; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.Info is not { } info) { continue; } + + affiliation *= 1f + info.GetSavedStatValue(StatTypes.Affiliation, Prefab.Identifier); + } + + return affiliation switch + { + >= 1f => FactionAffiliation.Affiliated, + _ => FactionAffiliation.Neutral + }; + } } internal class FactionPrefab : Prefab diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index fe45446d3..ed89cf0c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -70,6 +70,15 @@ namespace Barotrauma } reputationChange *= reputationGainMultiplier; } + else if (reputationChange < 0f) + { + float reputationLossMultiplier = 1f; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + reputationLossMultiplier += character.GetStatValue(StatTypes.ReputationLossMultiplier); + } + reputationChange *= reputationLossMultiplier; + } Value += reputationChange; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 1fa50625a..06d74e3fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -80,7 +80,7 @@ namespace Barotrauma public bool DisableEvents { - get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; } + get { return IsFirstRound && GameMain.GameSession.RoundDuration < FirstRoundEventDelay; } } public bool CheatsEnabled; @@ -139,6 +139,15 @@ namespace Barotrauma public virtual bool PurchasedLostShuttles { get; set; } public virtual bool PurchasedItemRepairs { get; set; } + private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) + { + if (GameMain.NetworkMember == null) { return true; } + //allow managing if no-one with permissions is alive + return + GameMain.NetworkMember.ConnectedClients.Count == 1 || + GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + } + protected CampaignMode(GameModePreset preset, CampaignSettings settings) : base(preset) { @@ -211,7 +220,7 @@ namespace Barotrauma return Level.Loaded?.StartLocation ?? Map.CurrentLocation; } - public List GetSubsToLeaveBehind(Submarine leavingSub) + public static List GetSubsToLeaveBehind(Submarine leavingSub) { //leave subs behind if they're not docked to the leaving sub and not at the same exit return Submarine.Loaded.FindAll(sub => @@ -240,7 +249,7 @@ namespace Barotrauma { for (int i = 0; i < wall.SectionCount; i++) { - wall.SetDamage(i, 0, createNetworkEvent: false); + wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false); } } } @@ -266,7 +275,7 @@ namespace Barotrauma wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); } - public int GetHullRepairCost() + public static int GetHullRepairCost() { float totalDamage = 0; foreach (Structure wall in Structure.WallList) @@ -283,7 +292,7 @@ namespace Barotrauma return (int)Math.Min(totalDamage * HullRepairCostPerDamage, MaxHullRepairCost); } - public int GetItemRepairCost() + public static int GetItemRepairCost() { float totalRepairDuration = 0.0f; foreach (Item item in Item.ItemList) @@ -316,9 +325,18 @@ namespace Barotrauma public override void AddExtraMissions(LevelData levelData) { + if (levelData == null) + { + throw new ArgumentException("Level data was null."); + } + extraMissions.Clear(); var currentLocation = Map.CurrentLocation; + if (currentLocation == null) + { + throw new InvalidOperationException("Current location was null."); + } if (levelData.Type == LevelData.LevelType.Outpost) { //if there's an available mission that takes place in the outpost, select it @@ -551,7 +569,7 @@ namespace Barotrauma /// /// Which submarine is at a position where it can leave the level and enter another one (if any). /// - private Submarine GetLeavingSub() + private static Submarine GetLeavingSub() { if (Level.IsLoadedOutpost) { @@ -652,9 +670,10 @@ namespace Barotrauma if (map != null && CargoManager != null) { map.CurrentLocation.RegisterTakenItems(takenItems); - map.CurrentLocation.AddStock(CargoManager.SoldItems); - CargoManager.ClearSoldItemsProjSpecific(); - map.CurrentLocation.RemoveStock(CargoManager.PurchasedItems); + if (transitionType != TransitionType.None) + { + UpdateStoreStock(); + } } if (GameMain.NetworkMember == null) { @@ -717,6 +736,16 @@ namespace Barotrauma } } + /// + /// Updates store stock before saving the game + /// + public void UpdateStoreStock() + { + Map?.CurrentLocation?.AddStock(CargoManager.SoldItems); + CargoManager?.ClearSoldItemsProjSpecific(); + Map?.CurrentLocation?.RemoveStock(CargoManager.PurchasedItems); + } + public void EndCampaign() { foreach (Character c in Character.CharacterList) @@ -740,6 +769,7 @@ namespace Barotrauma location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); location.Reset(); } + Map.ClearLocationHistory(); Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); if (Map.Radiation != null) @@ -1025,7 +1055,7 @@ namespace Barotrauma } } - protected void LeaveUnconnectedSubs(Submarine leavingSub) + protected static void LeaveUnconnectedSubs(Submarine leavingSub) { if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) { @@ -1084,6 +1114,7 @@ namespace Barotrauma if (item.Components.None(c => c is Pickable)) { continue; } if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; } if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } + if (item.Container?.GetComponent() is { DrawInventory: false }) { continue; } itemsToTransfer.Add((item, item.Container)); item.Submarine = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 1d96fa2fa..791d852b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -17,6 +18,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; + [Serialize(true, IsPropertySaveable.Yes)] + public bool TutorialEnabled { get; set; } + [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] public bool RadiationEnabled { get; set; } @@ -103,12 +107,9 @@ namespace Barotrauma private static int GetAddedMissionCount() { - int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); - } - return count; + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (!characters.Any()) { return 0; } + return characters.Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9e1e19aa6..a84b1a7c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -133,13 +133,13 @@ namespace Barotrauma } partial void InitProjSpecific(); - + public static string GetCharacterDataSavePath(string savePath) { - return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml"); + return Path.Combine(Path.GetDirectoryName(savePath), Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml"); } - public string GetCharacterDataSavePath() + public static string GetCharacterDataSavePath() { return GetCharacterDataSavePath(GameMain.GameSession.SavePath); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs index a9942470b..bb9438b28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs @@ -29,6 +29,14 @@ namespace Barotrauma public readonly Sprite Banner; + public readonly EndMessageInfo EndMessage; + + public enum EndType { None, Continue, Restart } + + public readonly record struct EndMessageInfo( + EndType EndType, + Identifier NextTutorialIdentifier); + public TutorialPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) { Order = element.GetAttributeInt("order", int.MaxValue); @@ -59,6 +67,13 @@ namespace Barotrauma } EventIdentifier = element.GetChildElement("scriptedevent")?.GetAttributeIdentifier("identifier", "") ?? Identifier.Empty; + + if (element.GetChildElement("endmessage") is ContentXElement endMessageElement) + { + EndMessage = new EndMessageInfo( + EndType: endMessageElement.GetAttributeEnum("type", EndType.None), + NextTutorialIdentifier: endMessageElement.GetAttributeIdentifier("nexttutorial", Identifier.Empty)); + } } public CharacterInfo GetTutorialCharacterInfo() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 46cdadcf0..a60f5b6fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -28,7 +28,10 @@ namespace Barotrauma private Location[]? dummyLocations; public CrewManager? CrewManager; - public double RoundStartTime; + public float RoundDuration + { + get; private set; + } public double TimeSpentCleaning, TimeSpentPainting; @@ -353,6 +356,7 @@ namespace Barotrauma #if DEBUG DateTime startTime = DateTime.Now; #endif + RoundDuration = 0.0f; AfflictionPrefab.LoadAllEffects(); MirrorLevel = mirrorLevel; @@ -503,7 +507,7 @@ namespace Barotrauma RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); - if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) + if (GameMode is not TutorialMode && GameMode is not TestGameMode) { GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false); if (EndLocation != null && levelData != null) @@ -573,9 +577,9 @@ namespace Barotrauma GameMode.Start(); foreach (Mission mission in missions) { - int prevEntityCount = Entity.GetEntities().Count(); + int prevEntityCount = Entity.GetEntities().Count; mission.Start(Level.Loaded); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount) { DebugConsole.ThrowError( $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + @@ -584,6 +588,9 @@ namespace Barotrauma } } +#if CLIENT + ObjectiveManager.ResetObjectives(); +#endif EventManager?.StartRound(Level.Loaded); SteamAchievementManager.OnStartRound(); @@ -611,7 +618,7 @@ namespace Barotrauma CreatureMetrics.Instance.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; - RoundStartTime = Timing.TotalTime; + RoundDuration = 0.0f; GameMain.ResetFrameTime(); IsRunning = true; } @@ -712,6 +719,7 @@ namespace Barotrauma public void Update(float deltaTime) { + RoundDuration += deltaTime; EventManager?.Update(deltaTime); GameMode?.Update(deltaTime); //backwards for loop because the missions may get completed and removed from the list in Update() @@ -761,7 +769,7 @@ namespace Barotrauma var result = GameMain.LuaCs.Hook.Call("getSessionCrewCharacters", type); if (result != null) return ImmutableHashSet.Create(result); - if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet.Empty; } + if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet.Empty; } IEnumerable players; IEnumerable bots; @@ -771,8 +779,8 @@ namespace Barotrauma players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer); #elif CLIENT - players = crewManager.GetCharacters().Where(c => c.IsPlayer); - bots = crewManager.GetCharacters().Where(c => c.IsBot); + players = crewManager.GetCharacters().Where(static c => c.IsPlayer); + bots = crewManager.GetCharacters().Where(static c => c.IsBot); #endif if (type.HasFlag(CharacterType.Bot)) { @@ -857,6 +865,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); } TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); + ObjectiveManager.ResetUI(); #endif SteamAchievementManager.OnRoundEnded(this); @@ -871,17 +880,16 @@ namespace Barotrauma #else bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif - double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, GameMode?.Preset.Identifier.Value ?? "none", - roundDuration); + RoundDuration); string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); - campaignMode.TotalPlayTime += roundDuration; + campaignMode.TotalPlayTime += RoundDuration; } #if CLIENT HintManager.OnRoundEnded(); @@ -907,21 +915,20 @@ namespace Barotrauma public void LogEndRoundStats(string eventId) { - double roundDuration = Timing.TotalTime - RoundStartTime; - GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count ?? 0), RoundDuration); foreach (Mission mission in missions) { - GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration); } if (Level.Loaded != null) { Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration); } if (Submarine.MainSub != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 91b63855f..2b40e6304 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -9,7 +9,7 @@ using Barotrauma.Networking; namespace Barotrauma { - internal partial class MedicalClinic + internal sealed partial class MedicalClinic { public enum NetworkHeader { @@ -18,7 +18,8 @@ namespace Barotrauma ADD_PENDING, REMOVE_PENDING, CLEAR_PENDING, - HEAL_PENDING + HEAL_PENDING, + ADD_EVERYTHING_TO_PENDING } public enum AfflictionSeverity @@ -43,23 +44,10 @@ namespace Barotrauma } [NetworkSerialize] - public struct NetHealRequest : INetSerializableStruct - { - public HealRequestResult Result; - } + public readonly record struct NetHealRequest(HealRequestResult Result) : INetSerializableStruct; [NetworkSerialize] - public struct NetRemovedAffliction : INetSerializableStruct - { - public NetCrewMember CrewMember; - public NetAffliction Affliction; - } - - public struct NetPendingCrew : INetSerializableStruct - { - [NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)] - public NetCrewMember[] CrewMembers; - } + public readonly record struct NetRemovedAffliction(NetCrewMember CrewMember, NetAffliction Affliction) : INetSerializableStruct; public struct NetAffliction : INetSerializableStruct { @@ -69,42 +57,18 @@ namespace Barotrauma [NetworkSerialize] public ushort Strength; + [NetworkSerialize] + public int VitalityDecrease; + [NetworkSerialize] public ushort Price; - public AfflictionSeverity AfflictionSeverity + public void SetAffliction(Affliction affliction, CharacterHealth characterHealth) { - get - { - if (Prefab is null) { return AfflictionSeverity.Low; } - - float normalizedStrength = Strength / Prefab.MaxStrength; - - // lesser than 0.1 - if (normalizedStrength <= 0.1) - { - return AfflictionSeverity.Low; - } - - // between 0.1 and 0.5 - if (normalizedStrength > 0.1f && normalizedStrength < 0.5f) - { - return AfflictionSeverity.Medium; - } - - // greater than 0.5 - return AfflictionSeverity.High; - } - } - - public Affliction Affliction - { - set - { - Identifier = value.Identifier; - Strength = (ushort)Math.Ceiling(value.Strength); - Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier); - } + Identifier = affliction.Identifier; + Strength = (ushort)Math.Ceiling(affliction.Strength); + Price = (ushort)(affliction.Prefab.BaseHealCost + Strength * affliction.Prefab.HealCostMultiplier); + VitalityDecrease = (int)affliction.GetVitalityDecrease(characterHealth); } private AfflictionPrefab? cachedPrefab; @@ -146,17 +110,23 @@ namespace Barotrauma } } - public struct NetCrewMember : INetSerializableStruct + public record struct NetCrewMember : INetSerializableStruct { [NetworkSerialize] public int CharacterInfoID; [NetworkSerialize] - public NetAffliction[] Afflictions; + public ImmutableArray Afflictions; - public CharacterInfo CharacterInfo + public NetCrewMember(CharacterInfo info) { - set => CharacterInfoID = value.GetIdentifierUsingOriginalName(); + CharacterInfoID = info.GetIdentifierUsingOriginalName(); + Afflictions = ImmutableArray.Empty; + } + + public NetCrewMember(CharacterInfo info, ImmutableArray afflictions): this(info) + { + Afflictions = afflictions; } public readonly CharacterInfo? FindCharacterInfo(ImmutableArray crew) @@ -194,11 +164,11 @@ namespace Barotrauma private static bool IsOutpostInCombat() { - if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } + if (Level.Loaded is not { Type: LevelData.LevelType.Outpost }) { return false; } - IEnumerable crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet(); + IEnumerable crew = GetCrewCharacters().Where(static c => c.Character != null).Select(static c => c.Character).ToImmutableHashSet(); - foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC)) + foreach (Character npc in Character.CharacterList.Where(static c => c.TeamID == CharacterTeamType.FriendlyNPC)) { bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy); if (isInCombatWithCrew) { return true; } @@ -238,6 +208,20 @@ namespace Barotrauma PendingHeals.Clear(); } + private void AddEverythingToPending() + { + foreach (CharacterInfo info in GetCrewCharacters()) + { + if (info.Character?.CharacterHealth is not { } health) { continue; } + + var afflictions = GetAllAfflictions(health); + + if (afflictions.Length is 0) { continue; } + + InsertPendingCrewMember(new NetCrewMember(info, afflictions)); + } + } + private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction) { foreach (NetCrewMember listMember in PendingHeals.ToList()) @@ -255,7 +239,7 @@ namespace Barotrauma newAfflictions.Add(pendingAffliction); } - pendingMember.Afflictions = newAfflictions.ToArray(); + pendingMember.Afflictions = newAfflictions.ToImmutableArray(); } if (!pendingMember.Afflictions.Any()) { continue; } @@ -280,9 +264,9 @@ namespace Barotrauma static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); } - private NetAffliction[] GetAllAfflictions(CharacterHealth health) + private ImmutableArray GetAllAfflictions(CharacterHealth health) { - IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a)); + IEnumerable rawAfflictions = health.GetAllAfflictions().Where(IsHealable); List afflictions = new List(); @@ -298,19 +282,20 @@ namespace Barotrauma } else { - newAffliction = new NetAffliction { Affliction = affliction }; + newAffliction = new NetAffliction(); + newAffliction.SetAffliction(affliction, health); newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price); } afflictions.Add(newAffliction); } - return afflictions.ToArray(); + return afflictions.ToImmutableArray(); static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } - public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price); + public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; @@ -325,7 +310,7 @@ namespace Barotrauma } #endif - return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray(); + return Character.CharacterList.Where(static c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(static c => c.Info).ToImmutableArray(); } #if DEBUG && CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs index 689569ff4..f1e1bc55f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs @@ -56,6 +56,10 @@ namespace Barotrauma } EndReadyCheck(); + +#if CLIENT + msgBox?.Close(); +#endif } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index a46d24b71..569cbac92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -207,7 +207,7 @@ namespace Barotrauma price = 0; } - if (Campaign.TryPurchase(client, price)) + if (force || Campaign.TryPurchase(client, price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -574,6 +574,7 @@ namespace Barotrauma } } + private readonly static HashSet upgradedSubs = new HashSet(); /// /// Applies an upgrade on the submarine, should be called by when the round starts. /// @@ -584,6 +585,12 @@ namespace Barotrauma /// New level that was applied, -1 if no upgrades were applied. private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine? parentSub = null) { + if (parentSub == null) + { + upgradedSubs.Clear(); + } + upgradedSubs.Add(submarine); + int? newLevel = null; if (category.IsWallUpgrade) { @@ -619,9 +626,12 @@ namespace Barotrauma } } - foreach (Submarine loadedSub in Submarine.Loaded.Where(sub => sub != submarine)) + foreach (Submarine loadedSub in Submarine.Loaded) { - if (loadedSub == parentSub) { continue; } + if (loadedSub == parentSub || loadedSub == submarine) { continue; } + if (loadedSub.Info?.Type != SubmarineType.Player) { continue; } + if (upgradedSubs.Contains(loadedSub)) { continue; } + XElement? root = loadedSub.Info?.SubmarineElement; if (root == null) { continue; } @@ -630,8 +640,8 @@ namespace Barotrauma if (root.Attribute("location") == null) { continue; } // Check if this is our linked submarine - ushort dockingPortID = (ushort) root.GetAttributeInt("originallinkedto", 0); - if (dockingPortID > 0 && submarine.GetItems(true).Any(item => item.ID == dockingPortID)) + ushort dockingPortID = (ushort)root.GetAttributeInt("originallinkedto", 0); + if (dockingPortID > 0 && submarine.GetItems(alsoFromConnectedSubs: true).Any(item => item.ID == dockingPortID)) { BuyUpgrade(prefab, category, loadedSub, level, submarine); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a660bbbf3..a9eabf2c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -51,7 +51,7 @@ namespace Barotrauma return slotString == null ? Array.Empty() : slotString.Split(','); } - public CharacterInventory(XElement element, Character character) + public CharacterInventory(XElement element, Character character, bool spawnInitialItems) : base(character, ParseSlotTypes(element).Length) { this.character = character; @@ -84,6 +84,8 @@ namespace Barotrauma InitProjSpecific(element); + if (!spawnInitialItems) { return; } + #if CLIENT //clients don't create items until the server says so if (GameMain.Client != null) { return; } @@ -94,7 +96,7 @@ namespace Barotrauma if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } string itemIdentifier = subElement.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab)) { DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found."); continue; @@ -200,6 +202,7 @@ namespace Barotrauma #if CLIENT CreateSlots(); #endif + CharacterHUD.RecreateHudTextsIfControlling(character); //if the item was equipped and there are more items in the same stack, equip one of those items if (tryEquipFromSameStack && wasEquipped) { @@ -493,7 +496,12 @@ namespace Barotrauma base.PutItem(item, i, user, removeItem, createNetworkEvent); #if CLIENT CreateSlots(); + if (character == Character.Controlled) + { + HintManager.OnObtainedItem(character, item); + } #endif + CharacterHUD.RecreateHudTextsIfControlling(character); if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) { item.CampaignInteractionType = CampaignMode.InteractionType.None; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 8188d52c9..64db689bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -63,6 +64,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.0f, IsPropertySaveable.No)] + public float RaycastRange { get; set; } + [Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { @@ -70,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.25f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] + public float Reload + { + get; + set; + } + [Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { @@ -77,6 +88,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreUser + { + get; + set; + } + private readonly List nodes = new List(); public IEnumerable Nodes { @@ -91,6 +109,10 @@ namespace Barotrauma.Items.Components private readonly Attack attack; + private Character user; + + private float reloadTimer; + public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { @@ -125,6 +147,7 @@ namespace Barotrauma.Items.Components charging = true; timer = Duration; IsActive = true; + user = character; #if SERVER if (GameMain.Server != null) { item.CreateServerEvent(this); } #endif @@ -144,6 +167,11 @@ namespace Barotrauma.Items.Components if (timer <= 0.0f) { + if (reloadTimer > 0.0f) + { + reloadTimer -= deltaTime; + return; + } IsActive = false; return; } @@ -196,6 +224,7 @@ namespace Barotrauma.Items.Components private void Discharge() { + reloadTimer = Reload; ApplyStatusEffects(ActionType.OnUse, 1.0f); FindNodes(item.WorldPosition, Range); if (attack != null) @@ -203,7 +232,7 @@ namespace Barotrauma.Items.Components foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } - character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); + character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); } } DischargeProjSpecific(); @@ -214,6 +243,18 @@ namespace Barotrauma.Items.Components public void FindNodes(Vector2 worldPosition, float range) { + if (RaycastRange > 0.0f) + { + float angle = 0.0f; + float dir = 1; + if (item.body != null) + { + angle += item.body.Rotation; + dir = item.body.Dir; + } + worldPosition += new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * RaycastRange * dir; + } + //see which submarines are within range so we can skip structures that are in far-away subs List submarinesInRange = new List(); foreach (Submarine sub in Submarine.Loaded) @@ -222,7 +263,7 @@ namespace Barotrauma.Items.Components { submarinesInRange.Add(sub); } - else + else if (sub != null) { Rectangle subBorders = new Rectangle( sub.Borders.X - (int)range, sub.Borders.Y + (int)range, @@ -235,7 +276,7 @@ namespace Barotrauma.Items.Components } } - //get all walls within range + //get all walls within range the arc could potentially hit List entitiesInRange = new List(100); foreach (Structure structure in Structure.WallList) { @@ -243,10 +284,10 @@ namespace Barotrauma.Items.Components if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; } var structureWorldRect = structure.WorldRect; - if (worldPosition.X < structureWorldRect.X - range) continue; - if (worldPosition.X > structureWorldRect.Right + range) continue; - if (worldPosition.Y > structureWorldRect.Y + range) continue; - if (worldPosition.Y < structureWorldRect.Y -structureWorldRect.Height - range) continue; + if (worldPosition.X < structureWorldRect.X - range) { continue; } + if (worldPosition.X > structureWorldRect.Right + range) { continue; } + if (worldPosition.Y > structureWorldRect.Y + range) { continue; } + if (worldPosition.Y < structureWorldRect.Y - structureWorldRect.Height - range) { continue; } if (structure.Submarine != null) { @@ -263,26 +304,51 @@ namespace Barotrauma.Items.Components entitiesInRange.Add(structure); } + + nodes.Clear(); + if (RaycastRange > 0.0f) + { + nodes.Add(new Node(item.WorldPosition, -1)); + int parentNodeIndex = 0; + AddNodesBetweenPoints(item.WorldPosition, worldPosition, 0.5f, ref parentNodeIndex); + } + else + { + nodes.Add(new Node(worldPosition, -1)); + } + + //get all characters within range the arc could potentially hit + float totalRange = RaycastRange + range; foreach (Character character in Character.CharacterList) { - if (!character.Enabled) continue; - if (OutdoorsOnly && character.Submarine != null) continue; - if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue; + if (!character.Enabled) { continue; } + if (IgnoreUser && character == user) { continue; } + if (OutdoorsOnly && character.Submarine != null) { continue; } + if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; } - if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls) + if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls) { entitiesInRange.Add(character); } + //if the weapon does a raycast, check distance to the ray too (not just the end of the ray) + if (RaycastRange > 0) + { + float distSqr = MathUtils.LineSegmentToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition); + //if the distance from the initial raycast to the character is small (e.g. goes through the character), we know it must hit + if (distSqr < range * range * RangeMultiplierInWalls) + { + if (!entitiesInRange.Contains(character)) { entitiesInRange.Add(character); } + charactersInRange.Add((character, nodes.First())); + } + } } - nodes.Clear(); - nodes.Add(new Node(worldPosition, -1)); - FindNodes(entitiesInRange, worldPosition, 0, range); + FindNodes(entitiesInRange, worldPosition, nodes.Count - 1, range); //construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge) for (int i = 0; i < nodes.Count; i++) { - if (nodes[i].ParentIndex < 0) continue; + if (nodes[i].ParentIndex < 0) { continue; } Node parentNode = nodes[nodes[i].ParentIndex]; float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f); float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition); @@ -292,7 +358,7 @@ namespace Barotrauma.Items.Components private void FindNodes(List entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange) { - if (currentRange <= 0.0f || nodes.Count >= MaxNodes) return; + if (currentRange <= 0.0f || nodes.Count >= MaxNodes) { return; } //find the closest structure int closestIndex = -1; @@ -322,7 +388,7 @@ namespace Barotrauma.Items.Components } else if (entitiesInRange[i] is Character character) { - dist = Vector2.Distance(character.WorldPosition, currPos); + dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition); } if (dist < closestDist) @@ -434,20 +500,35 @@ namespace Barotrauma.Items.Components for (int j = 0; j < entitiesInRange.Count; j++) { var otherEntity = entitiesInRange[j]; - if (!(otherEntity is Character character)) continue; - if (OutdoorsOnly && character.Submarine != null) continue; + if (otherEntity is not Character character) { continue; } + if (IgnoreUser && character == user) { continue; } + if (OutdoorsOnly && character.Submarine != null) { continue; } + Vector2 characterMin = new Vector2(character.AnimController.Limbs.Min(l => l.WorldPosition.X), character.AnimController.Limbs.Min(l => l.WorldPosition.Y)); + Vector2 characterMax = new Vector2(character.AnimController.Limbs.Max(l => l.WorldPosition.X), character.AnimController.Limbs.Max(l => l.WorldPosition.Y)); if (targetStructure.IsHorizontal) { - if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue; - if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue; - if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue; + if (characterMax.X < targetStructure.WorldRect.X) { continue; } + if (characterMin.X > targetStructure.WorldRect.Right) { continue; } + if (Math.Abs(characterMin.Y - targetStructure.WorldPosition.Y) > currentRange && + Math.Abs(characterMax.Y - targetStructure.WorldPosition.Y) > currentRange) + { + continue; + } } else { - if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue; - if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue; - if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue; + if (characterMax.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; } + if (characterMin.Y > targetStructure.WorldRect.Y) { continue; } + if (Math.Abs(characterMin.X - targetStructure.WorldPosition.X) > currentRange && + Math.Abs(characterMax.X - targetStructure.WorldPosition.X) > currentRange) + { + continue; + } + } + if (!charactersInRange.Any(c => c.character == character)) + { + charactersInRange.Add((character, nodes[parentNodeIndex])); } float closestNodeDistSqr = float.MaxValue; int closestNodeIndex = -1; @@ -473,7 +554,10 @@ namespace Barotrauma.Items.Components AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex); nodes.Add(new Node(targetPos, parentNodeIndex)); entitiesInRange.RemoveAt(closestIndex); - charactersInRange.Add((character, nodes[parentNodeIndex])); + if (!charactersInRange.Any(c => c.character == character)) + { + charactersInRange.Add((character, nodes[parentNodeIndex])); + } FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange); } } @@ -483,7 +567,7 @@ namespace Barotrauma.Items.Components Vector2 diff = targetPos - currPos; float dist = diff.Length(); Vector2 normal = new Vector2(-diff.Y, diff.X) / dist; - for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.5f)) + for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.0f)) { //0 at the edges, 1 at the center float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 1c5986b4d..c2b5630bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components set { attachedByDefault = value; } } - [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")] public Vector2 HoldPos { @@ -143,7 +143,11 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } +#if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif public float HoldAngle { get { return MathHelper.ToDegrees(holdAngle); } @@ -151,23 +155,50 @@ namespace Barotrauma.Items.Components } private Vector2 swingAmount; +#if DEBUG [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] +#else + [Serialize("0.0,0.0", IsPropertySaveable.No)] +#endif public Vector2 SwingAmount { get { return ConvertUnits.ToDisplayUnits(swingAmount); } set { swingAmount = ConvertUnits.ToSimUnits(value); } } - +#if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif + public float SwingSpeed { get; set; } +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenHolding { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenAiming { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenUsing { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No)] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool DisableHeadRotation { get; set; } [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] @@ -650,11 +681,10 @@ namespace Barotrauma.Items.Components return false; } Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true); - Structure attachTarget = Structure.GetAttachTarget(attachPos); - + Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine; int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); int currentlyAttachedCount = Item.ItemList.Count( - i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); + i => i.Submarine == attachSubmarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); if (maxAttachableCount == 0) { #if CLIENT @@ -812,10 +842,18 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (item.body == null || !item.body.Enabled) { return; } + + Character owner = picker ?? item.GetRootInventoryOwner() as Character; + + if (owner != null) + { + ApplyStatusEffects(ActionType.OnActive, deltaTime, owner); + } + if (picker == null || !picker.HasEquippedItem(item)) { if (Pusher != null) { Pusher.Enabled = false; } - if (attachTargetCell == null) { IsActive = false; } + if (attachTargetCell == null && owner == null) { IsActive = false; } return; } @@ -824,23 +862,7 @@ namespace Barotrauma.Items.Components Drawable = true; } - Vector2 swing = Vector2.Zero; - if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) - { - swingState += deltaTime; - swingState %= 1.0f; - if (SwingWhenHolding || - (SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) || - (SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot))) - { - swing = swingAmount * new Vector2( - PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f, - PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f); - } - } - - ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - + UpdateSwingPos(deltaTime, out Vector2 swingPos); if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); @@ -853,7 +875,7 @@ namespace Barotrauma.Items.Components scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle); + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); if (!aim) { var rope = GetRope(); @@ -890,6 +912,24 @@ namespace Barotrauma.Items.Components } } + public void UpdateSwingPos(float deltaTime, out Vector2 swingPos) + { + swingPos = Vector2.Zero; + if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) + { + swingState += deltaTime; + swingState %= 1.0f; + if (SwingWhenHolding || + (SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) || + (SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot))) + { + swingPos = swingAmount * new Vector2( + PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f, + PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f); + } + } + } + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 5bfd54d81..238e4b6c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -214,8 +214,9 @@ namespace Barotrauma.Items.Components bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; if (aim) { + UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); if (ac.InWater) { ac.LockFlippingUntil = (float)Timing.TotalTime + Reload; @@ -314,6 +315,7 @@ namespace Barotrauma.Items.Components if (f2.Body.UserData is Limb targetLimb) { if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; } + if (targetLimb.character.IgnoreMeleeWeapons) { return false; } var targetCharacter = targetLimb.character; if (targetCharacter == picker) { return false; } if (AllowHitMultiple) @@ -329,6 +331,7 @@ namespace Barotrauma.Items.Components else if (f2.Body.UserData is Character targetCharacter) { if (targetCharacter == picker || targetCharacter == User) { return false; } + if (targetCharacter.IgnoreMeleeWeapons) { return false; } targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways if (AllowHitMultiple) { @@ -392,37 +395,38 @@ namespace Barotrauma.Items.Components float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier); + Character user = User; Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target); if (Attack != null) { - Attack.SetUser(User); + Attack.SetUser(user); Attack.DamageMultiplier = damageMultiplier; if (targetLimb != null) { if (targetLimb.character.Removed) { return; } targetLimb.character.LastDamageSource = item; - Attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f); + Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f); } else if (targetCharacter != null) { if (targetCharacter.Removed) { return; } targetCharacter.LastDamageSource = item; - Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); + Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f); } else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure) { if (targetStructure.Removed) { return; } - Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); + Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f); } else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } - var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { Character.Controlled?.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, @@ -436,7 +440,7 @@ namespace Barotrauma.Items.Components else if (target.UserData is Holdable holdable && holdable.CanPush) { if (holdable.Item.Removed) { return; } - Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f); + Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f); RestoreCollision(); hitting = false; User = null; @@ -449,29 +453,32 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User); - -#if SERVER - if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits + ActionType conditionalActionType = ActionType.OnSuccess; + if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user)) { - GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData( - success ? ActionType.OnUse : ActionType.OnFailure, - targetItemComponent: null, - targetCharacter, targetLimb)); - - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + conditionalActionType = ActionType.OnFailure; + } + if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null) + { + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb)); + #if SERVER + if (GameMain.Server != null) //TODO: Log structure hits + { + string logStr = picker?.LogName + " used " + item.Name; + if (item.ContainedItems != null && item.ContainedItems.Any()) + { + logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; + } + logStr += " on " + targetCharacter.LogName + "."; + Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + } + #endif } -#endif - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? { - ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index cadea84be..0c45b1548 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components { public enum UseEnvironment { - Air, Water, Both + Air, Water, Both, None }; private float useState; @@ -23,6 +23,8 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; set; } + [Serialize(true, IsPropertySaveable.No, description: "If the item is held in RightHand or LeftHand, apply extra force there")] + public bool ApplyToHands { get; set; } #if CLIENT private string particles; [Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")] @@ -42,6 +44,7 @@ namespace Barotrauma.Items.Components { if (character == null || character.Removed) { return false; } if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; } + if (UsableIn == UseEnvironment.None) { return false; } IsActive = true; useState = 0.1f; @@ -70,13 +73,16 @@ namespace Barotrauma.Items.Components character.AnimController.Collider.ApplyForce(propulsion); - if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) - { - character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); - } - if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) - { - character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); + if (ApplyToHands) + { + if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) + { + character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); + } + if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) + { + character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); + } } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 998d7d7c1..0ac7dcac2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -32,6 +32,20 @@ namespace Barotrauma.Items.Components set { reload = Math.Max(value, 0.0f); } } + [Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")] + public float ReloadSkillRequirement + { + get; + set; + } + + [Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")] + public float ReloadNoSkill + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")] public bool HoldTrigger { @@ -39,7 +53,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; @@ -60,6 +74,23 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched). Sum of weapon + projectile.")] + public float LaunchImpulse + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs). Sum of weapon + projectile."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] + public float Penetration { get; private set; } + + [Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")] + public float WeaponDamageModifier + { + get; + private set; + } + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { @@ -99,6 +130,12 @@ namespace Barotrauma.Items.Components // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; characterUsable = true; + + if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) + { + DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement."); + } + InitProjSpecific(element); } @@ -167,7 +204,15 @@ namespace Barotrauma.Items.Components if (currentChargeTime < MaxChargeTime) { return false; } IsActive = true; - ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); + float baseReloadTime = reload; + float weaponSkill = character.GetSkillLevel("weapons"); + if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement) + { + //Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5 + float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1); + baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); + } + ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); currentChargeTime = 0f; if (character != null) @@ -218,15 +263,18 @@ namespace Barotrauma.Items.Components { lastProjectile?.Item.GetComponent()?.Snap(); } - float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier); + float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier; projectile.Launcher = item; - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); - if (i == 0) + if (projectile.Item.body != null) { - Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + if (i == 0) + { + Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); Item.RemoveContained(projectile.Item); } LastProjectile = projectile; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index e7658a80b..bf808c48a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -636,11 +636,14 @@ namespace Barotrauma.Items.Components float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT - Character.Controlled?.UpdateHUDProgressBar( - this, - targetItem.WorldPosition, - levelResource.DeattachTimer / levelResource.DeattachDuration, - GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); + if (targetItem.Prefab.ShowHealthBar) + { + Character.Controlled?.UpdateHUDProgressBar( + this, + targetItem.WorldPosition, + levelResource.DeattachTimer / levelResource.DeattachDuration, + GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); + } #endif FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index df595ca57..edf91b18e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -1,17 +1,26 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { class Throwable : Holdable { - private float throwPos; - private bool throwing, throwDone; + enum ThrowState + { + None, + Initiated, + Throwing + } + + private const float ThrowAngleStart = -MathHelper.PiOver2, ThrowAngleEnd = MathHelper.PiOver2; + private float throwAngle = ThrowAngleStart; private bool midAir; + private ThrowState throwState; + + //continuous collision detection is used while the item is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -27,7 +36,6 @@ namespace Barotrauma.Items.Components public Throwable(Item item, ContentXElement element) : base(item, element) { - //throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f); if (aimPos == Vector2.Zero) { aimPos = new Vector2(0.6f, 0.1f); @@ -36,22 +44,21 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems + //actual throwing logic is handled in Update + return characterUsable || character == null; } public override bool SecondaryUse(float deltaTime, Character character = null) { - if (!throwDone) return false; //This should only be triggered in update - throwDone = false; - return true; + //actual throwing logic is handled in Update - SecondaryUse only triggers when the item is thrown + return false; } public override void Drop(Character dropper) { base.Drop(dropper); - - throwing = false; - throwPos = 0.0f; + throwState = ThrowState.None; + throwAngle = ThrowAngleStart; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -100,13 +107,22 @@ namespace Barotrauma.Items.Components return; } - if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; } - if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; } - bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; + if (throwState != ThrowState.Throwing) + { + if (picker.IsKeyDown(InputType.Aim)) + { + if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; } + } + else if (throwState != ThrowState.Initiated) + { + throwAngle = ThrowAngleStart; + } + } + bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; if (picker.IsDead || !picker.AllowInput) { - throwing = false; + throwState = ThrowState.None; aim = false; } @@ -124,25 +140,29 @@ namespace Barotrauma.Items.Components item.Submarine = picker.Submarine; - if (!throwing) + if (throwState != ThrowState.Throwing) { - if (aim) + if (aim || throwState == ThrowState.Initiated) { - throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); + throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated) + { + throwState = ThrowState.Throwing; + } } else { - throwPos = 0; + throwAngle = ThrowAngleStart; ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { - throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); + throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); - if (throwPos < 0) + if (throwAngle < 0) { Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition); //throw upwards if cursor is at the position of the character @@ -180,8 +200,7 @@ namespace Barotrauma.Items.Components Limb rightHand = ac.GetLimb(LimbType.RightHand); item.body.AngularVelocity = rightHand.body.AngularVelocity; - throwPos = 0; - throwDone = true; + throwAngle = ThrowAngleStart; IsActive = true; if (GameMain.NetworkMember is { IsServer: true }) @@ -193,7 +212,7 @@ namespace Barotrauma.Items.Components //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); } - throwing = false; + throwState = ThrowState.None; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 154734f0f..d8196a344 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -113,6 +113,13 @@ namespace Barotrauma.Items.Components private bool drawable = true; + [Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)] + public PropertyConditional.Comparison IsActiveConditionalComparison + { + get; + set; + } + public List IsActiveConditionals; public bool Drawable @@ -243,6 +250,20 @@ namespace Barotrauma.Items.Components [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } + + /// + /// Can be used by status effects or conditionals to the speed of the item + /// + public float Speed + { + get + { + return item.Speed; + } + } + + public readonly bool InheritStatusEffects; + public ItemComponent(Item item, ContentXElement element) { this.item = item; @@ -303,6 +324,7 @@ namespace Barotrauma.Items.Components string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", ""); if (!string.IsNullOrEmpty(inheritStatusEffectsFrom)) { + InheritStatusEffects = true; var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase)); if (component == null) { @@ -799,7 +821,14 @@ namespace Barotrauma.Items.Components } else { - hasRequiredItems = itemList.Any(Predicate); + if (itemList.Any(Predicate)) + { + hasRequiredItems = !relatedItem.RequireEmpty; + } + else + { + hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty; + } if (!hasRequiredItems) { shouldBreak = true; @@ -816,7 +845,7 @@ 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, float applyOnUserFraction = 0.0f) + 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) { if (statusEffectLists == null) { return; } @@ -830,11 +859,6 @@ namespace Barotrauma.Items.Components if (user != null) { effect.SetUser(user); } effect.AfflictionMultiplier = afflictionMultiplier; item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); - if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character)) - { - effect.AfflictionMultiplier = applyOnUserFraction; - item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition); - } effect.AfflictionMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index a1d69ce93..d76db314d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -47,6 +47,13 @@ namespace Barotrauma.Items.Components { return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab)); } + + public bool MatchesItem(Identifier identifierOrTag) + { + return + ContainableItems == null || ContainableItems.Count == 0 || + ContainableItems.Any(c => c.Identifiers.Contains(identifierOrTag) && !c.ExcludedIdentifiers.Contains(identifierOrTag)); + } } public readonly NamedEvent OnContainedItemsChanged = new NamedEvent(); @@ -65,8 +72,16 @@ namespace Barotrauma.Items.Components public int Capacity { get { return capacity; } - set { capacity = Math.Max(value, 0); } + private set + { + capacity = Math.Max(value, 0); + MainContainerCapacity = value; + } } + /// + /// The capacity of the main container without taking the sub containers into account. Only differs when there's a sub container defined for the component. + /// + public int MainContainerCapacity { get; private set; } //how many items can be contained private int maxStackSize; @@ -99,7 +114,7 @@ namespace Barotrauma.Items.Components [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")] public bool DrawInventory { get; @@ -127,9 +142,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(true, IsPropertySaveable.No)] - public bool AllowAccess { get; set; } - [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } @@ -229,6 +241,9 @@ namespace Barotrauma.Items.Components public ImmutableHashSet ContainableItemIdentifiers => containableItemIdentifiers; public List ContainableItems { get; } + public List AllSubContainableItems { get; } + + public readonly bool HasSubContainers; public ItemContainer(Item item, ContentXElement element) : base(item, element) @@ -251,6 +266,7 @@ namespace Barotrauma.Items.Components break; case "subcontainer": totalCapacity += subElement.GetAttributeInt("capacity", 1); + HasSubContainers = true; break; } } @@ -270,7 +286,7 @@ namespace Barotrauma.Items.Components int subCapacity = subElement.GetAttributeInt("capacity", 1); int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); - List subContainableItems = null; + var subContainableItems = new List(); foreach (var subSubElement in subElement.Elements()) { if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } @@ -281,8 +297,9 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } - subContainableItems ??= new List(); subContainableItems.Add(containable); + AllSubContainableItems ??= new List(); + AllSubContainableItems.Add(containable); } for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) @@ -357,6 +374,14 @@ namespace Barotrauma.Items.Components //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + + if (IsActive && item.GetRootInventoryOwner() is Character owner && + owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) + { + // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). + SetContainedActive(true); + } + CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -368,9 +393,9 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); - //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -409,6 +434,20 @@ namespace Barotrauma.Items.Components return false; } + public override void FlipX(bool relativeToSub) + { + base.FlipX(relativeToSub); + if (HideItems) { return; } + if (item.body == null) { return; } + foreach (Item containedItem in Inventory.AllItems) + { + if (containedItem.body != null && containedItem.body.Enabled && containedItem.body.Dir != item.body.Dir) + { + containedItem.FlipX(relativeToSub); + } + } + } + public override void Update(float deltaTime, Camera cam) { if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) @@ -441,6 +480,7 @@ namespace Barotrauma.Items.Components { foreach (Item item in Inventory.AllItemsMod) { + item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter); item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); item.GetComponent()?.Equip(ownerCharacter); autoInjectCooldown = AutoInjectInterval; @@ -477,7 +517,7 @@ namespace Barotrauma.Items.Components effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets)); + effect.AddNearbyTargets(item.WorldPosition, targets); effect.Apply(ActionType.OnActive, deltaTime, item, targets); } } @@ -494,12 +534,12 @@ namespace Barotrauma.Items.Components public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } public override bool Select(Character character) { - if (!AllowAccess) { return false; } + if (!DrawInventory) { return false; } if (item.Container != null) { return false; } if (AccessOnlyWhenBroken) { @@ -535,7 +575,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - if (!AllowAccess) { return false; } + if (!DrawInventory) { return false; } if (AccessOnlyWhenBroken) { if (item.Condition > 0) @@ -582,11 +622,65 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { IsActive = true; + SetContainedActive(false); } public override void Equip(Character character) { IsActive = true; + if (character != null && character.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) + { + SetContainedActive(true); + } + } + + private void SetContainedActive(bool active) + { + foreach (Item containedItem in Inventory.AllItems) + { + RelatedItem containableItem = FindContainableItem(containedItem); + if (containableItem != null && containableItem.SetActive) + { + foreach (var ic in containedItem.Components) + { + ic.IsActive = active; + } + if (containedItem.body != null) + { + containedItem.body.Enabled = active; + if (active) + { + containedItem.body.PhysEnabled = false; + } + } + } + } + if (active) + { + FlipX(false); + } + } + + private RelatedItem FindContainableItem(Item item) + { + var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); + if (relatedItem == null && AllSubContainableItems != null) + { + relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item)); + } + return relatedItem; + } + + /// + /// Returns the index of the first slot whose restrictions match the specified tag or identifier + /// + public int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier) + { + for (int i = 0; i < slotRestrictions.Length; i++) + { + if (slotRestrictions[i].MatchesItem(itemTagOrIdentifier)) { return i; } + } + return null; } public override void ReceiveSignal(Signal signal, Connection connection) @@ -604,6 +698,7 @@ namespace Barotrauma.Items.Components } } +#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods. public void SetContainedItemPositions() { Vector2 transformedItemPos = ItemPos * item.Scale; @@ -657,29 +752,70 @@ namespace Barotrauma.Items.Components transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); transformedItemPos += item.Position; } - } - - float currentRotation = itemRotation; - if (item.body != null) - { - currentRotation *= item.body.Dir; - currentRotation += item.body.Rotation; - } - else - { - currentRotation += -item.RotationRad; } int i = 0; Vector2 currentItemPos = transformedItemPos; foreach (Item contained in Inventory.AllItems) { + Vector2 itemPos = currentItemPos; + var relatedItem = FindContainableItem(contained); + if (relatedItem != null) + { + if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } + if (relatedItem.ItemPos.HasValue) + { + Vector2 pos = relatedItem.ItemPos.Value; + if (item.body != null) + { + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.Position; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) + { + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; + } + if (item.FlippedY) + { + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; + } + } + } + } + if (contained.body != null) { try { - Vector2 simPos = ConvertUnits.ToSimUnits(currentItemPos); - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation); + Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); + float rotation = itemRotation; + if (relatedItem != null && relatedItem.Rotation != 0) + { + rotation = MathHelper.ToRadians(relatedItem.Rotation); + } + if (item.body != null) + { + rotation *= item.body.Dir; + rotation += item.body.Rotation; + } + else + { + rotation += -item.RotationRad; + } + contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); contained.body.UpdateDrawPosition(); } @@ -695,8 +831,8 @@ namespace Barotrauma.Items.Components contained.Rect = new Rectangle( - (int)(currentItemPos.X - contained.Rect.Width / 2.0f), - (int)(currentItemPos.Y + contained.Rect.Height / 2.0f), + (int)(itemPos.X - contained.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Rect.Height / 2.0f), contained.Rect.Width, contained.Rect.Height); contained.Submarine = item.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index e11973068..0499ce20d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -450,6 +450,7 @@ namespace Barotrauma.Items.Components public override bool Select(Character activator) { if (activator == null || activator.Removed) { return false; } + if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; } if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater || UsableIn == UseEnvironment.Air && activator.AnimController.InWater) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 82aa1676a..5da741dcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -104,12 +104,14 @@ namespace Barotrauma.Items.Components // doesn't quite work properly, remaining time changes if tinkering stops float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); + float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); + if (DeconstructItemsSimultaneously) { float deconstructTime = 0.0f; foreach (Item targetItem in inputContainer.Inventory.AllItems) { - deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier); + deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -139,7 +141,7 @@ namespace Barotrauma.Items.Components if (targetItem == null) { return; } var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList(); - float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) @@ -218,7 +220,7 @@ namespace Barotrauma.Items.Components if (percentageHealth < deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } - if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) + if (MapEntityPrefab.FindByIdentifier(deconstructProduct.ItemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); return; @@ -460,6 +462,12 @@ namespace Barotrauma.Items.Components progressTimer = 0.0f; progressState = 0.0f; } +#if CLIENT + else + { + HintManager.OnStartDeconstructing(user, this); + } +#endif inputContainer.Inventory.Locked = IsActive; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 13f858f37..d6ae48ec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -30,11 +30,8 @@ namespace Barotrauma.Items.Components Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { - get { return maxForce; } - set - { - maxForce = Math.Max(0.0f, value); - } + get => maxForce; + set => maxForce = Math.Max(0.0f, value); } [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, @@ -94,7 +91,7 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(ContentXElement element); - + public override void Update(float deltaTime, Camera cam) { UpdateOnActiveEffects(deltaTime); @@ -129,12 +126,14 @@ namespace Barotrauma.Items.Components { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } - currForce *= maxForce * forceMultiplier; - if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; + if (item.GetComponent() is { IsTinkering: true } repairable) { currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } + currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce); + //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 9047d2f5e..0c41133e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components { private ImmutableDictionary fabricationRecipes; //this is not readonly because tutorials fuck this up!!!! + private const int MaxAmountToFabricate = 99; + private FabricationRecipe fabricatedItem; private float timeUntilReady; private float requiredTime; @@ -39,6 +41,16 @@ namespace Barotrauma.Items.Components [Serialize(1.0f, IsPropertySaveable.Yes)] public float SkillRequirementMultiplier { get; set; } + private int amountToFabricate; + [Serialize(1, IsPropertySaveable.Yes)] + public int AmountToFabricate + { + get { return amountToFabricate; } + set { amountToFabricate = MathHelper.Clamp(value, 1, MaxAmountToFabricate); } + } + + private int amountRemaining; + private const float TinkeringSpeedIncrease = 2.5f; private enum FabricatorState @@ -89,7 +101,7 @@ namespace Barotrauma.Items.Components { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; - } + } } var fabricationRecipes = new Dictionary(); @@ -104,6 +116,18 @@ namespace Barotrauma.Items.Components continue; } } + + bool recipeInvalid = false; + foreach (var requiredItem in recipe.RequiredItems) + { + if (requiredItem.ItemPrefabs.None()) + { + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\"."); + recipeInvalid = true; + } + } + if (recipeInvalid) { continue; } + fabricationRecipes.Add(recipe.RecipeHash, recipe); if (recipe.FabricationLimitMax >= 0) { @@ -171,16 +195,20 @@ namespace Barotrauma.Items.Components if (selectedItem == null) { return; } if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } -#if CLIENT - itemList.Enabled = false; - activateButton.Text = TextManager.Get("FabricatorCancel"); -#endif - IsActive = true; this.user = user; fabricatedItem = selectedItem; RefreshAvailableIngredients(); +#if CLIENT + itemList.Enabled = false; + if (amountInput != null) + { + amountInput.Enabled = false; + } + RefreshActivateButtonText(); +#endif + bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; if (!isClient) { @@ -237,10 +265,11 @@ namespace Barotrauma.Items.Components } #elif CLIENT itemList.Enabled = true; - if (activateButton != null) + if (amountInput != null) { - activateButton.Text = TextManager.Get(CreateButtonText); + amountInput.Enabled = true; } + RefreshActivateButtonText(); #endif fabricatedItem = null; } @@ -356,9 +385,10 @@ namespace Barotrauma.Items.Components bool ingredientsStolen = false; bool ingredientsAllowStealing = true; - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer) { - fabricatedItem.RequiredItems.ForEach(requiredItem => + List foundAvailableItems = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) { for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { @@ -367,10 +397,7 @@ namespace Barotrauma.Items.Components if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availableItems = availableIngredients[requiredPrefab.Identifier]; - var availableItem = availableItems.FirstOrDefault(potentialPrefab => - { - return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); - }); + var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage)); if (availableItem == null) { continue; } @@ -401,13 +428,21 @@ namespace Barotrauma.Items.Components } } + foundAvailableItems.Add(availableItem); availableItems.Remove(availableItem); - Entity.Spawner.AddItemToRemoveQueue(availableItem); - inputContainer.Inventory.RemoveItem(availableItem); break; } } - }); + } + + var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems); + user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); + + foreach (Item availableItem in fabricationIngredients.Items) + { + Entity.Spawner.AddItemToRemoveQueue(availableItem); + inputContainer.Inventory.RemoveItem(availableItem); + } int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); @@ -500,20 +535,16 @@ namespace Barotrauma.Items.Components } } - //disabled "continuous fabrication" for now - //before we enable it, there should be some UI controls for fabricating a specific number of items - - /*var prevFabricatedItem = fabricatedItem; + var prevFabricatedItem = fabricatedItem; var prevUser = user; CancelFabricating(); - if (CanBeFabricated(prevFabricatedItem)) + + amountRemaining--; + if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser)) { //keep fabricating if we can fabricate more StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false); - }*/ - - - CancelFabricating(); + } } } @@ -535,12 +566,13 @@ namespace Barotrauma.Items.Components return currPowerConsumption; } - private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { - if (user == null) { return 0; } + if (user?.Info == null) { return 0; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; + floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false); foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); @@ -571,11 +603,25 @@ namespace Barotrauma.Items.Components } partial void UpdateRequiredTimeProjSpecific(); + + private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) + { + return + (user != null && user.HasRecipeForItem(item.Identifier)) || + GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); + } private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } - if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } + if (fabricableItem.RequiresRecipe) + { + if (character == null) { return false; } + if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem)) + { + return false; + } + } if (fabricableItem.RequiredMoney > 0) { @@ -637,9 +683,15 @@ namespace Barotrauma.Items.Components //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster - return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f); + float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); + + if (user?.Info is { } info && fabricableItem.TargetItem is { } it) + { + time /= 1f + it.Tags.Sum(tag => info.GetSavedStatValue(StatTypes.FabricationSpeed, tag)); + } + return time; } - + public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { if (skills.Length == 0) { return 1.0f; } @@ -700,7 +752,7 @@ namespace Barotrauma.Items.Components itemList.AddRange(container.Inventory.AllItems); } } - if (user?.Inventory != null) + if (user?.Inventory != null && user.SelectedItem == item) { itemList.AddRange(user.Inventory.AllItems); linkedInventories.Add(user.Inventory); @@ -713,7 +765,31 @@ namespace Barotrauma.Items.Components { availableIngredients[itemIdentifier] = new List(itemList.Count); } - availableIngredients[itemIdentifier].Add(item); + //order by condition (prefer using worst-condition items) + int index = 0; + while (index < availableIngredients[itemIdentifier].Count && + compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0) + { + index++; + } + + static int compare(Item item1, Item item2, Inventory inputInventory) + { + bool item1InInputInventory = item1.ParentInventory == inputInventory; + bool item2InInputInventory = item2.ParentInventory == inputInventory; + //prefer items in the input inventory + if (item1InInputInventory != item2InInputInventory) + { + return item1InInputInventory ? 1 : -1; + } + else + { + //prefer items in worse condition + return Math.Sign(item2.Condition - item1.Condition); + } + } + + availableIngredients[itemIdentifier].Insert(index, item); } } @@ -827,5 +903,15 @@ namespace Barotrauma.Items.Components public float Value { get; set; } public ItemPrefab ItemPrefab { get; set; } } + + internal sealed class AbilityFabricationItemIngredients : AbilityObject + { + public List Items { get; set; } + + public AbilityFabricationItemIngredients(List items) + { + Items = items; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index a3588ac5c..c7d8b37e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components public float? ReceivedOxygenAmount, ReceivedWaterAmount; + public double LastOxygenDataTime, LastWaterDataTime; + public readonly HashSet Cards = new HashSet(); public bool Distort; @@ -23,12 +25,8 @@ namespace Barotrauma.Items.Components public List LinkedHulls = new List(); } - private DateTime resetDataTime; - private bool hasPower; - private readonly Dictionary hullDatas; - [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] public bool RequireWaterDetectors { @@ -75,7 +73,6 @@ namespace Barotrauma.Items.Components : base(item, element) { IsActive = true; - hullDatas = new Dictionary(); InitProjSpecific(); } @@ -83,37 +80,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - //periodically reset all hull data - //(so that outdated hull info won't be shown if detectors stop sending signals) - if (DateTime.Now > resetDataTime) - { - foreach (HullData hullData in hullDatas.Values) - { - if (!hullData.Distort) - { - hullData.ReceivedOxygenAmount = null; - hullData.ReceivedWaterAmount = null; - } - } - resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); - } - -#if CLIENT - if (cardRefreshTimer > cardRefreshDelay) - { - if (item.Submarine is { } sub) - { - UpdateIDCards(sub); - } - - cardRefreshTimer = 0; - } - else - { - cardRefreshTimer += deltaTime; - } -#endif - hasPower = Voltage > MinVoltage; if (hasPower) { @@ -138,65 +104,5 @@ namespace Barotrauma.Items.Components { return picker != null; } - - public override void ReceiveSignal(Signal signal, Connection connection) - { - Item source = signal.source; - if (source == null || source.CurrentHull == null) { return; } - - Hull sourceHull = source.CurrentHull; - if (!hullDatas.TryGetValue(sourceHull, out HullData hullData)) - { - hullData = new HullData(); - hullDatas.Add(sourceHull, hullData); - } - - if (hullData.Distort) { return; } - - switch (connection.Name) - { - case "water_data_in": - //cheating a bit because water detectors don't actually send the water level - bool fromWaterDetector = source.GetComponent() != null; - hullData.ReceivedWaterAmount = null; - if (fromWaterDetector) - { - hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); - } - foreach (var linked in sourceHull.linkedTo) - { - if (!(linked is Hull linkedHull)) { continue; } - if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) - { - linkedHullData = new HullData(); - hullDatas.Add(linkedHull, linkedHullData); - } - linkedHullData.ReceivedWaterAmount = null; - if (fromWaterDetector) - { - linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull); - } - } - break; - case "oxygen_data_in": - if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy)) - { - oxy = Rand.Range(0.0f, 100.0f); - } - hullData.ReceivedOxygenAmount = oxy; - foreach (var linked in sourceHull.linkedTo) - { - if (!(linked is Hull linkedHull)) { continue; } - if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) - { - linkedHullData = new HullData(); - hullDatas.Add(linkedHull, linkedHullData); - } - linkedHullData.ReceivedOxygenAmount = oxy; - } - break; - } - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index db2eae084..923623a3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -57,8 +57,8 @@ namespace Barotrauma.Items.Components [Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { - get { return maxFlow; } - set { maxFlow = value; } + get => maxFlow; + set => maxFlow = value; } [Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] @@ -92,13 +92,16 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(ContentXElement element); - + public override void Update(float deltaTime, Camera cam) { pumpSpeedLockTimer -= deltaTime; isActiveLockTimer -= deltaTime; - if (!IsActive) { return; } + if (!IsActive) + { + return; + } currFlow = 0.0f; @@ -122,7 +125,10 @@ namespace Barotrauma.Items.Components FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } - if (!HasPower) { return; } + if (!HasPower) + { + return; + } UpdateProjSpecific(deltaTime); @@ -132,13 +138,15 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); - currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; + currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; - if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + if (item.GetComponent() is { IsTinkering: true } repairable) { currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; } + currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow); + //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 7fc66baf2..d19f9d7dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { const float NetworkUpdateIntervalHigh = 0.5f; - const float TemperatureBoostAmount = 20; + const float TemperatureBoostAmount = 25; //the rate at which the reactor is being run on (higher rate -> higher temperature) private float fissionRate; @@ -26,10 +26,6 @@ namespace Barotrauma.Items.Components //amount of power generated balanced with the load) private bool autoTemp; - //automatical adjustment to the power output when - //turbine output and temperature are in the optimal range - private float autoAdjustAmount; - private float fuelConsumptionRate; private float meltDownTimer, meltDownDelay; @@ -53,6 +49,8 @@ namespace Barotrauma.Items.Components private float temperatureBoost; + public bool AllowTemperatureBoost => Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f; + private bool _powerOn; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] @@ -95,15 +93,12 @@ namespace Barotrauma.Items.Components } } } - + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { - get { return maxPowerOutput; } - set - { - maxPowerOutput = Math.Max(0.0f, value); - } + get => maxPowerOutput; + set => maxPowerOutput = Math.Max(0.0f, value); } [Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")] @@ -152,11 +147,11 @@ namespace Barotrauma.Items.Components turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f); } } - + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] public float FuelConsumptionRate { - get { return fuelConsumptionRate; } + get => fuelConsumptionRate; set { if (!MathUtils.IsValid(value)) return; @@ -256,6 +251,8 @@ namespace Barotrauma.Items.Components } #endif + float maxPowerOut = GetMaxOutput(); + if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1) { TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f); @@ -289,9 +286,9 @@ namespace Barotrauma.Items.Components //use a smoothed "correct output" instead of the actual correct output based on the load //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily - if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f)) + if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f)) { - CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; + CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; } //calculate tolerances of the meters based on the skills of the user @@ -315,7 +312,7 @@ namespace Barotrauma.Items.Components Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff)); temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime); #if CLIENT - temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f; + temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = AllowTemperatureBoost; #endif FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime); @@ -350,7 +347,7 @@ namespace Barotrauma.Items.Components if (!isConnectedToFriendlyOutpost) { - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime; } } fuelLeft += item.ConditionPercentage; @@ -359,10 +356,10 @@ namespace Barotrauma.Items.Components if (fissionRate > 0.0f) { - if (item.AiTarget != null && MaxPowerOutput > 0) + if (item.AiTarget != null && maxPowerOut > 0) { var aiTarget = item.AiTarget; - float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; + float range = Math.Abs(currPowerConsumption) / maxPowerOut; aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); if (item.CurrentHull != null) { @@ -433,15 +430,17 @@ namespace Barotrauma.Items.Components tolerance = 3f; } + float maxPowerOut = GetMaxOutput(); + float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); - float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); - float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); + float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); + float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); minUpdatePowerOut = minOutput; maxUpdatePowerOut = maxOutput; - float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut; - + float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut; + return new PowerRange(minOutput, maxOutput, reactorMax); } @@ -464,11 +463,13 @@ namespace Barotrauma.Items.Components float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut); float newLoad = loadLeft; + float maxOutput = GetMaxOutput(); + //Adjust behaviour for multi reactor setup - if (MaxPowerOutput != minMaxPower.ReactorMaxOutput) + if (maxOutput != minMaxPower.ReactorMaxOutput) { - float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; - float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); + float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft; + float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100); newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft); } @@ -509,7 +510,7 @@ namespace Barotrauma.Items.Components //calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate); float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f); - float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput; + float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput(); //maximum output not enough, we need more fuel return theoreticalMaxOutput < Load * minimumOutputRatio; @@ -693,7 +694,7 @@ namespace Barotrauma.Items.Components aiUpdateTimer = AIUpdateInterval; // load more fuel if the current maximum output is only 50% of the current load // or if the fuel rod is (almost) deplenished - float minCondition = fuelConsumptionRate * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); + float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition)) { bool outOfFuel = false; @@ -871,5 +872,8 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; } } } + + private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); + private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 077ae053f..807c13d23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -151,15 +151,7 @@ namespace Barotrauma.Items.Components set { bool changed = currentMode != value; - currentMode = value; - if (value == Mode.Passive) - { - if (item.AiTarget != null) - { - item.AiTarget.SectorDegrees = 360.0f; - } - } #if CLIENT if (changed) { prevPassivePingRadius = float.MaxValue; } UpdateGUIElements(); @@ -206,13 +198,6 @@ namespace Barotrauma.Items.Components var activePing = activePings[currentPingIndex]; if (activePing.State > 1.0f) { - if (item.AiTarget != null) - { - float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom); - item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range); - item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f; - item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); - } aiPingCheckPending = true; currentPingIndex = -1; } @@ -228,21 +213,27 @@ namespace Barotrauma.Items.Components activePings[currentPingIndex].Direction = pingDirection; activePings[currentPingIndex].State = 0.0f; activePings[currentPingIndex].PrevPingRadius = 0.0f; + if (item.AiTarget != null) + { + item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; + item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); + } item.Use(deltaTime); } } else { - if (item.AiTarget != null) - { - item.AiTarget.SectorDegrees = 360.0f; - } aiPingCheckPending = false; } } for (var pingIndex = 0; pingIndex < activePingsCount;) { + if (item.AiTarget != null) + { + float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); + item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range)); + } if (activePings[pingIndex].State > 1.0f) { var lastIndex = --activePingsCount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 75776f814..90922ec50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -144,6 +144,11 @@ namespace Barotrauma.Items.Components } } + public float TargetVelocityLengthSquared + { + get => TargetVelocity.LengthSquared(); + } + public Vector2 SteeringInput { get { return steeringInput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index 24a780d6e..2e432496a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -248,6 +248,7 @@ namespace Barotrauma.Items.Components if (container?.Inventory == null) { return; } + bool recreateHudTexts = false; for (var i = 0; i < container.Inventory.Capacity; i++) { if (i < 0 || GrowableSeeds.Length <= i) { continue; } @@ -257,6 +258,7 @@ namespace Barotrauma.Items.Components if (growable != null) { + recreateHudTexts |= GrowableSeeds[i] != growable; GrowableSeeds[i] = growable; growable.IsActive = true; } @@ -267,11 +269,14 @@ namespace Barotrauma.Items.Components // Kill the plant if it's somehow removed oldGrowable.Decayed = true; oldGrowable.IsActive = false; + recreateHudTexts = true; } - GrowableSeeds[i] = null; } } +#if CLIENT + CharacterHUD.RecreateHudTexts |= recreateHudTexts; +#endif // server handles this if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 67c2f4a94..4c90945ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Items.Components { //[power/min] private float capacity; + private float adjustedCapacity; private float charge, prevCharge; @@ -65,8 +66,12 @@ namespace Barotrauma.Items.Components [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] public float Capacity { - get { return capacity; } - set { capacity = Math.Max(value, 1.0f); } + get => capacity; + set + { + capacity = Math.Max(value, 1.0f); + adjustedCapacity = GetCapacity(); + } } [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")] @@ -76,10 +81,10 @@ namespace Barotrauma.Items.Components set { if (!MathUtils.IsValid(value)) return; - charge = MathHelper.Clamp(value, 0.0f, capacity); + charge = MathHelper.Clamp(value, 0.0f, adjustedCapacity); //send a network event if the charge has changed by more than 5% - if (Math.Abs(charge - lastSentCharge) / capacity > 0.05f) + if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f) { #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); } @@ -89,7 +94,7 @@ namespace Barotrauma.Items.Components } } - public float ChargePercentage => MathUtils.Percentage(Charge, Capacity); + public float ChargePercentage => MathUtils.Percentage(Charge, adjustedCapacity); [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed @@ -125,10 +130,19 @@ namespace Barotrauma.Items.Components set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); } } + private bool flipIndicator; + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge be flipped to fill from the other side.")] + public bool FlipIndicator + { + get { return flipIndicator; } + set { flipIndicator = value; } + } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; private bool isRunning; + public bool HasBeenTuned { get; private set; } public PowerContainer(Item item, ContentXElement element) @@ -146,8 +160,9 @@ namespace Barotrauma.Items.Components return picker != null; } - public override void Update(float deltaTime, Camera cam) + public override void Update(float deltaTime, Camera cam) { + adjustedCapacity = GetCapacity(); if (item.Connections == null) { IsActive = false; @@ -155,7 +170,7 @@ namespace Barotrauma.Items.Components } isRunning = true; - float chargeRatio = charge / capacity; + float chargeRatio = charge / adjustedCapacity; if (chargeRatio > 0.0f) { @@ -171,7 +186,7 @@ namespace Barotrauma.Items.Components item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out"); item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); - item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); + item.SendSignal(((int)Math.Round(Charge / adjustedCapacity * 100)).ToString(), "charge_%"); item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate"); } @@ -184,16 +199,16 @@ namespace Barotrauma.Items.Components if (connection == powerIn) { //Don't draw power if fully charged - if (charge >= capacity) + if (charge >= adjustedCapacity) { - charge = capacity; + charge = adjustedCapacity; return 0; } else { if (item.Condition <= 0.0f) { return 0.0f; } - float missingCharge = capacity - charge; + float missingCharge = adjustedCapacity - charge; float targetRechargeSpeed = rechargeSpeed; if (ExponentialRechargeSpeed) @@ -230,7 +245,7 @@ namespace Barotrauma.Items.Components if (connection == powerOut) { float maxOutput; - float chargeRatio = prevCharge / capacity; + float chargeRatio = prevCharge / adjustedCapacity; if (chargeRatio < 0.1f) { maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; @@ -283,7 +298,7 @@ namespace Barotrauma.Items.Components else { //Decrease charge based on how much power is leaving the device - Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, adjustedCapacity); prevCharge = Charge; } } @@ -370,5 +385,7 @@ namespace Barotrauma.Items.Components } } } + + public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index e355ccb14..88db0df19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components { if (!powerOnSoundPlayed && powerOnSound != null) { - SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling); + SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling, freqMult: powerOnSound.GetRandomFrequencyMultiplier()); powerOnSoundPlayed = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index b2d26005a..d31b52edb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -266,37 +266,43 @@ namespace Barotrauma.Items.Components if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", Projectile", item); } + + if (item.body == null) + { + DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!"); + return; + } + InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { - if (item.body != null) + if (item.body == null) { return; } + if (Attack != null && Attack.DamageRange <= 0.0f) { - if (Attack != null && Attack.DamageRange <= 0.0f) + switch (item.body.BodyShape) { - switch (item.body.BodyShape) - { - case PhysicsBody.Shape.Circle: - Attack.DamageRange = item.body.radius; - break; - case PhysicsBody.Shape.Capsule: - Attack.DamageRange = item.body.height / 2 + item.body.radius; - break; - case PhysicsBody.Shape.Rectangle: - Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); - break; - } - Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); + case PhysicsBody.Shape.Circle: + Attack.DamageRange = item.body.radius; + break; + case PhysicsBody.Shape.Capsule: + Attack.DamageRange = item.body.height / 2 + item.body.radius; + break; + case PhysicsBody.Shape.Rectangle: + Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); + break; } - originalCollisionCategories = item.body.CollisionCategories; - originalCollisionTargets = item.body.CollidesWith; + Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); } + originalCollisionCategories = item.body.CollisionCategories; + originalCollisionTargets = item.body.CollidesWith; } private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f) { + if (Item.body == null) { return; } Item.body.ResetDynamics(); Item.SetTransform(simPosition, rotation); if (Attack != null) @@ -354,6 +360,7 @@ namespace Barotrauma.Items.Components public bool Use(Character character = null, float launchImpulseModifier = 0f) { if (character != null && !characterUsable) { return false; } + if (item.body == null) { return false; } for (int i = 0; i < HitScanCount; i++) { @@ -392,6 +399,7 @@ namespace Barotrauma.Items.Components } } User = character; + ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User); return true; } @@ -416,14 +424,7 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; - - item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; - if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive) - { - item.body.CollidesWith |= Physics.CollisionProjectile; - } - + EnableProjectileCollisions(); IsActive = true; if (stickJoint == null) { return; } @@ -548,11 +549,10 @@ namespace Barotrauma.Items.Components } else if (fixture?.Body == null || fixture.IsSensor) { - //ignore sensors and items + //ignore sensors return true; } if (fixture.Body.UserData is VineTile) { return true; } - if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub @@ -562,13 +562,28 @@ namespace Barotrauma.Items.Components if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; } } - //ignore everything else than characters, sub walls and level walls - if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) && - !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && - !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; } - if (fixture.Body.UserData is VoronoiCell && (this.item.Submarine != null || submarine != null)) { return true; } + if (fixture.Body.UserData is Item item) + { + if (item == Item) { return true; } + if (item.Condition <= 0) { return true; } + if (!item.Prefab.DamagedByProjectiles && item.GetComponent() == null) { return true; } + } + else if (fixture.Body.UserData is Holdable { CanPush: false }) + { + // Ignore holdables that can't push -> shouldn't block + return true; + } + else + { + // TODO: This might make us ignore something we don't want to ignore? + // Not item -> ignore everything else than characters, sub walls and level walls + if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) && + !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && + !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; } + } + fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform); if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; } @@ -585,20 +600,16 @@ namespace Barotrauma.Items.Components } else if (fixture?.Body == null || fixture.IsSensor) { - //ignore sensors and items + //ignore sensors return -1; } if (fixture.Body.UserData is VineTile) { return -1; } - - if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } - if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; } - if (!(fixture.Body.UserData is Holdable holdable && holdable.CanPush)) + if (fixture.Body.UserData is Item item) { - //ignore everything else than characters, sub walls and level walls - if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) && - !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && - !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } + if (item.Condition <= 0) { return -1; } + if (!item.Prefab.DamagedByProjectiles && item.GetComponent() == null) { return -1; } } + if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub if (submarine != null) @@ -608,6 +619,12 @@ namespace Barotrauma.Items.Components if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; } } + // Ignore holdables that can't push -> shouldn't block + if (fixture.Body.UserData is Holdable { CanPush: false }) + { + return -1; + } + //ignore level cells if the item and the point of impact are inside a sub if (fixture.Body.UserData is VoronoiCell) { @@ -638,7 +655,7 @@ namespace Barotrauma.Items.Components hits.Add(new HitscanResult(fixture, point, normal, fraction)); return 1; - }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking); + }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile); return hits; } @@ -736,6 +753,7 @@ namespace Barotrauma.Items.Components { return false; } + if (target.IsSensor) { return false; } if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine) { @@ -757,6 +775,12 @@ namespace Barotrauma.Items.Components else if (target.Body.UserData is Item item) { if (item.Condition <= 0.0f) { return false; } + if (!item.Prefab.DamagedByProjectiles) { return false; } + } + else if (target.Body.UserData is Holdable { CanPush: false }) + { + // Ignore holdables that can't push -> shouldn't block + return false; } //ignore character colliders (the projectile only hits limbs) @@ -772,7 +796,7 @@ namespace Barotrauma.Items.Components { item.body.FarseerBody.ResetDynamics(); } - if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) + if (hits.Count >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) { DisableProjectileCollisions(); return true; @@ -881,7 +905,7 @@ namespace Barotrauma.Items.Components { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { Character.Controlled?.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, @@ -915,23 +939,22 @@ namespace Barotrauma.Items.Components if (character != null) { character.LastDamageSource = item; } - ActionType actionType = ActionType.OnUse; - if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user)) + ActionType conditionalActionType = ActionType.OnSuccess; + if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User)) { - actionType = ActionType.OnFailure; + conditionalActionType = ActionType.OnFailure; } - #if CLIENT - PlaySound(actionType, user: _user); - PlaySound(ActionType.OnImpact, user: _user); + PlaySound(conditionalActionType, user: User); + PlaySound(ActionType.OnImpact, user: User); #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user); + ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User); var attack = targetLimb.attack; if (attack != null) { @@ -940,8 +963,6 @@ namespace Barotrauma.Items.Components { if (effect.type == ActionType.OnImpact) { - //effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); - if (effect.HasTargetType(StatusEffect.TargetType.This)) { effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); @@ -950,32 +971,27 @@ namespace Barotrauma.Items.Components effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets)); + effect.AddNearbyTargets(targetLimb.WorldPosition, targets); effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); } - } } } -#if SERVER - if (GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true } server) { - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); } -#endif } else { - ApplyStatusEffects(actionType, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); -#if SERVER - if (GameMain.NetworkMember.IsServer) + ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User); + if (GameMain.NetworkMember is { IsServer: true } server) { - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); } -#endif } } @@ -1053,8 +1069,19 @@ namespace Barotrauma.Items.Components return true; } + private void EnableProjectileCollisions() + { + item.body.CollisionCategories = Physics.CollisionProjectile; + item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + if (!IgnoreProjectilesWhileActive) + { + item.body.CollidesWith |= Physics.CollisionProjectile; + } + } + private void DisableProjectileCollisions() { + if (item?.body?.FarseerBody == null) { return; } item.body.FarseerBody.OnCollision -= OnProjectileCollision; if (originalCollisionCategories != Category.None && originalCollisionTargets != Category.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index fbdb9e7c4..75426b030 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -9,14 +10,6 @@ namespace Barotrauma.Items.Components { public const int MaxQuality = 3; - public static readonly float[] QualityCommonnesses = new float[] - { - 0.8f, - 0.15f, - 0.045f, - 0.005f, - }; - public enum StatType { Condition, @@ -81,5 +74,29 @@ namespace Barotrauma.Items.Components if (!statValues.ContainsKey(statType)) { return 0.0f; } return statValues[statType] * qualityLevel; } + + /// + /// Get a random quality for an item spawning in some sub, taking into account the type of the submarine and the difficulty of the current level + /// (high-quality items become more common as difficulty increases) + /// + public static int GetSpawnedItemQuality(Submarine submarine, Level level, Rand.RandSync randSync = Rand.RandSync.ServerAndClient) + { + if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; } + + float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f); + return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync); + + static float GetCommonness(int quality, float difficultyFactor) + { + return quality switch + { + 0 => 1, + 1 => MathHelper.Lerp(0.0f, 1f, difficultyFactor), + 2 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.15f, 0f)), //15 difficulty transition to next biome - unlock Excellent loot + 3 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.35f, 0f)), //35 difficulty transition to next biome - unlock Masterwork loot + _ => 0.0f, + }; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e486300c6..e5604f449 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -420,7 +421,8 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { - item.Condition -= DeteriorationSpeed * deltaTime; + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; } } return; @@ -467,8 +469,14 @@ namespace Barotrauma.Items.Components wasGoodCondition = true; } + float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed); + if (requiredSkills.Any(static skill => skill.Identifier == "mechanical")) + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + } + float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); - fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; + fixDuration /= 1 + talentMultiplier + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); @@ -500,7 +508,7 @@ namespace Barotrauma.Items.Components SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f)); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); - CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); + CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item)); } if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); @@ -687,4 +695,14 @@ namespace Barotrauma.Items.Components //where set_active/set_state signals can disable the component } } + + internal sealed class AbilityRepairable : AbilityObject, IAbilityItem + { + public Item Item { get; set; } + + public AbilityRepairable(Item item) + { + Item = item; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 32f151379..1ccd035f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null) + if (item.Container != null && !(item.GetRootInventoryOwner() is Character)) { SetLightSourceState(false, 0.0f); return; @@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components if (body != null && !body.Enabled) { SetLightSourceState(false, 0.0f); - return; + return; } //currPowerConsumption = powerConsumption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index e60825beb..012548826 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -65,14 +65,7 @@ namespace Barotrauma.Items.Components { get { - if (GameMain.GameSession != null) - { - return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime); - } - else - { - return 0.0f; - } + return GameMain.GameSession?.RoundDuration ?? 0.0f; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index df767e009..3b98d93bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -649,7 +649,7 @@ namespace Barotrauma.Items.Components var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; //use linked projectile containers in case they have to react to the turret being launched somehow //(play a sound, spawn more projectiles) - if (!(e is Item linkedItem)) { continue; } + if (e is not Item linkedItem) { continue; } if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.Condition <= 0.0f) { @@ -692,7 +692,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { - if (!(e is Item linkedItem)) { continue; } + if (e is not Item linkedItem) { continue; } if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) { @@ -872,7 +872,7 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); - private void ShiftItemsInProjectileContainer(ItemContainer container) + private static void ShiftItemsInProjectileContainer(ItemContainer container) { if (container == null) { return; } bool moved; @@ -1063,8 +1063,8 @@ namespace Barotrauma.Items.Components character.AIController.SelectTarget(null); } - bool canShoot = true; - if (!HasPowerToShoot()) + bool canShoot = HasPowerToShoot(); + if (!canShoot) { List batteries = GetDirectlyConnectedBatteries(); float lowestCharge = 0.0f; @@ -1089,7 +1089,6 @@ namespace Barotrauma.Items.Components character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value, identifier: "supercapacitorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); - canShoot = false; } } } @@ -1104,7 +1103,6 @@ namespace Barotrauma.Items.Components character.Speak(TextManager.Get("DialogTurretHasNoPower").Value, identifier: "turrethasnopower".ToIdentifier(), minDurationBetweenSimilar: 30.0f); - canShoot = false; } } @@ -1283,7 +1281,7 @@ namespace Barotrauma.Items.Components closestDistance = shootDistance; foreach (var wall in Level.Loaded.ExtraWalls) { - if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; } + if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } foreach (var cell in wall.Cells) { if (cell.DoesDamage) @@ -1405,19 +1403,18 @@ namespace Barotrauma.Items.Components Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. Body worldTarget = CheckLineOfSight(start, end); - bool shoot; if (closestEnemy != null && closestEnemy.Submarine != null) { start -= closestEnemy.Submarine.SimPosition; end -= closestEnemy.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); + canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); } else { - shoot = CanShoot(worldTarget, character); + canShoot = CanShoot(worldTarget, character); } - if (!shoot) { return false; } + if (!canShoot) { return false; } if (character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogFireTurret").Value, @@ -1471,6 +1468,7 @@ namespace Barotrauma.Items.Components { if (targetBody.UserData is ISpatialEntity e) { + if (e is Structure s && s.Indestructible) { return false; } Submarine sub = e.Submarine ?? e as Submarine; if (!targetSubmarines && e is Submarine) { return false; } if (sub == null) { return false; } @@ -1559,7 +1557,7 @@ namespace Barotrauma.Items.Components return projectiles; } - private void CheckProjectileContainer(Item projectileContainer, List projectiles, out bool stopSearching) + private static void CheckProjectileContainer(Item projectileContainer, List projectiles, out bool stopSearching) { stopSearching = false; if (projectileContainer.Condition <= 0.0f) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 57b4dc685..67084c123 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -270,6 +270,9 @@ namespace Barotrauma.Items.Components public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + public bool AllowUseWhenWorn { get; set; } + public readonly int Variants; private int variant; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 8ffc22676..8390437d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -226,6 +226,14 @@ namespace Barotrauma { foreach (var item in slots[i].Items) { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); +#endif + continue; + } + bool duplicateFound = false; for (int j = 0; j < i; j++) { @@ -573,11 +581,13 @@ namespace Barotrauma if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } #endif + CharacterHUD.RecreateHudTextsIfControlling(user); if (item.body != null) { item.body.Enabled = false; item.body.BodyType = FarseerPhysics.BodyType.Dynamic; + item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false); } #if SERVER @@ -926,6 +936,7 @@ namespace Barotrauma if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } #endif + CharacterHUD.RecreateHudTextsIfFocused(item); } } @@ -952,6 +963,12 @@ namespace Barotrauma slots[index].RemoveItem(item); } + public bool IsInSlot(Item item, int index) + { + if (index < 0 || index >= slots.Length) { return false; } + return slots[index].Contains(item); + } + public void SharedRead(IReadMessage msg, out List[] newItemIds) { byte slotCount = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 52d556c2e..b8e075e03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -139,7 +139,7 @@ namespace Barotrauma private ConcurrentQueue impactQueue; //a dictionary containing lists of the status effects in all the components of the item - private readonly bool[] hasStatusEffectsOfType; + private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; public Dictionary SerializableProperties { get; protected set; } @@ -425,6 +425,39 @@ namespace Barotrauma public Color? HighlightColor; + /// + /// Can be used by status effects or conditionals to check whether the item is contained inside something + /// + public bool IsContained + { + get + { + return parentInventory != null; + } + } + + /// + /// Can be used by status effects or conditionals to the speed of the item + /// + public float Speed + { + get + { + if (body != null && body.PhysEnabled) + { + return body.LinearVelocity.Length(); + } + else if (ParentInventory?.Owner is Character character) + { + return character.AnimController.MainLimb.LinearVelocity.Length(); + } + else if (container != null) + { + return container.Speed; + } + return 0.0f; + } + } [Serialize("", IsPropertySaveable.Yes)] @@ -598,7 +631,7 @@ namespace Barotrauma { if (!spawnedInCurrentOutpost && value) { - OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? ""; + OriginalOutpost = GameMain.GameSession?.LevelData?.Seed; } spawnedInCurrentOutpost = value; } @@ -619,7 +652,9 @@ namespace Barotrauma set { originalOutpost = value; - if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value) + if (!string.IsNullOrEmpty(value) && + GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.LevelData?.Seed == value) { spawnedInCurrentOutpost = true; } @@ -822,6 +857,18 @@ namespace Barotrauma public bool IsSecondaryItem { get; } + private ItemStatManager statManager; + public ItemStatManager StatManager + { + get + { + statManager ??= new ItemStatManager(this); + return statManager; + } + } + + public Action OnDeselect; + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -963,7 +1010,6 @@ namespace Barotrauma } } - hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; foreach (ItemComponent ic in components) { if (ic is Pickable pickable) @@ -975,12 +1021,15 @@ namespace Barotrauma } if (ic.statusEffectLists == null) { continue; } - - if (statusEffectLists == null) + if (ic.InheritStatusEffects) { - statusEffectLists = new Dictionary>(); + // Inherited status effects are added when the ItemComponent is initialized at ItemComponent.cs:332. + // Don't create duplicate effects here. + continue; } + statusEffectLists ??= new Dictionary>(); + //go through all the status effects of the component //and add them to the corresponding statuseffect list foreach (List componentEffectList in ic.statusEffectLists.Values) @@ -1047,6 +1096,12 @@ namespace Barotrauma } } + var holdables = components.Where(c => c is Holdable); + if (holdables.Count() > 1) + { + DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))})."); + } + InsertToList(); ItemList.Add(this); if (Prefab.IsDangerous) { dangerousItems.Add(this); } @@ -1061,7 +1116,6 @@ namespace Barotrauma GameMain.LuaCs.Hook.Call("item.created", this); ApplyStatusEffects(ActionType.OnSpawn, 1.0f); - Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); RecalculateConditionValues(); #if CLIENT Submarine.ForceVisibilityRecheck(); @@ -1576,11 +1630,7 @@ namespace Barotrauma public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null) { - if (effect.intervalTimer > 0.0f) - { - effect.intervalTimer -= deltaTime; - return; - } + if (effect.ShouldWaitForInterval(this, deltaTime)) { return; } if (!isNetworkEvent && checkCondition) { if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; } @@ -1614,7 +1664,7 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems)) { - targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets)); + effect.AddNearbyTargets(WorldPosition, targets); if (targets.Count > 0) { hasTargets = true; @@ -1840,16 +1890,32 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - bool shouldBeActive = true; - foreach (var conditional in ic.IsActiveConditionals) + if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) { - if (!ConditionalMatches(conditional)) + bool shouldBeActive = true; + foreach (var conditional in ic.IsActiveConditionals) { - shouldBeActive = false; - break; + if (!ConditionalMatches(conditional)) + { + shouldBeActive = false; + break; + } } + ic.IsActive = shouldBeActive; + } + else + { + bool shouldBeActive = false; + foreach (var conditional in ic.IsActiveConditionals) + { + if (ConditionalMatches(conditional)) + { + shouldBeActive = true; + break; + } + } + ic.IsActive = shouldBeActive; } - ic.IsActive = shouldBeActive; } #if CLIENT if (ic.HasSounds) @@ -2075,7 +2141,7 @@ namespace Barotrauma } //no need to apply buoyancy if the item is still and not light enough to float - if (moving || body.Density < 10.0f) + if (moving || body.Density <= 10.0f) { Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity; body.ApplyForce(buoyancy); @@ -2102,12 +2168,15 @@ namespace Barotrauma if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } } - contact.GetWorldManifold(out Vector2 normal, out _); - if (contact.FixtureA.Body == f1.Body) { normal = -normal; } - float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); + if (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > 1.0f) + { + contact.GetWorldManifold(out Vector2 normal, out _); + if (contact.FixtureA.Body == f1.Body) { normal = -normal; } + float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); + impactQueue ??= new ConcurrentQueue(); + impactQueue.Enqueue(impact); + } - impactQueue ??= new ConcurrentQueue(); - impactQueue.Enqueue(impact); isActive = true; return true; @@ -2711,39 +2780,27 @@ namespace Barotrauma return; } #endif - - float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f; - bool remove = false; foreach (ItemComponent ic in components) { if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; } bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user); - ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure; + ActionType conditionalActionType = success ? ActionType.OnSuccess : ActionType.OnFailure; #if CLIENT - ic.PlaySound(actionType, user); + ic.PlaySound(conditionalActionType, user); + ic.PlaySound(ActionType.OnUse, user); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction); - if (applyOnSelfFraction > 0.0f) - { - //hacky af - ic.statusEffectLists.TryGetValue(actionType, out var effectList); - if (effectList != null) - { - effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction); - ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user); - effectList.ForEach(e => e.AfflictionMultiplier = 1.0f); - } - } + ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData( - actionType, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb)); } if (ic.DeleteOnUse) { remove = true; } @@ -2753,7 +2810,6 @@ namespace Barotrauma { var abilityItem = new AbilityApplyTreatment(user, character, this); user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem); - } if (remove) { Spawner?.AddItemToRemoveQueue(this); } @@ -2846,11 +2902,14 @@ namespace Barotrauma } foreach (ItemComponent ic in components) { ic.Equip(character); } + + CharacterHUD.RecreateHudTextsIfControlling(character); } public void Unequip(Character character) { foreach (ItemComponent ic in components) { ic.Unequip(character); } + CharacterHUD.RecreateHudTextsIfControlling(character); } public List<(object obj, SerializableProperty property)> GetProperties() @@ -2879,15 +2938,20 @@ namespace Barotrauma //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); SerializableProperty property = extraData.SerializableProperty; + ISerializableEntity entity = extraData.Entity; if (property != null) { - var propertyOwner = allProperties.Find(p => p.property == property); if (allProperties.Count > 1) { - msg.WriteByte((byte)allProperties.FindIndex(p => p.property == property)); + int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity); + if (propertyIndex < -1) + { + throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\""); + } + msg.WriteVariableUInt32((uint)propertyIndex); } - object value = property.GetValue(propertyOwner.obj); + object value = property.GetValue(entity); if (value is string stringVal) { msg.WriteString(stringVal); @@ -2992,7 +3056,7 @@ namespace Barotrauma int propertyIndex = 0; if (allProperties.Count > 1) { - propertyIndex = msg.ReadByte(); + propertyIndex = (int)msg.ReadVariableUInt32(); } bool allowEditing = true; @@ -3136,15 +3200,14 @@ namespace Barotrauma } logPropertyChangeCoroutine = CoroutineManager.Invoke(() => { - if(sender.Character != null) - GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); + GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); }, delay: 1.0f); } #endif - if (GameMain.NetworkMember is { IsServer: true }) + if (GameMain.NetworkMember is { IsServer: true } && parentObject is ISerializableEntity entity) { - GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property)); + GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property, entity)); } } @@ -3248,7 +3311,7 @@ namespace Barotrauma { if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) { - GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property)); + GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property, item)); } } } @@ -3367,12 +3430,6 @@ namespace Barotrauma item.PurchasedNewSwap = false; } - item.condition = element.GetAttributeFloat("condition", item.condition); - item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); - item.lastSentCondition = item.condition; - item.RecalculateConditionValues(); - item.SetActiveSprite(); - Version savedVersion = submarine?.Info.GameVersion; if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase)) { @@ -3380,14 +3437,41 @@ namespace Barotrauma //(the sub may have already been saved and up-to-date, even though the character inventories aren't) savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0")); } - + + float prevCondition = item.condition; if (savedVersion != null) { SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion); } + if (element.GetAttribute("conditionpercentage") != null) + { + item.condition = element.GetAttributeFloat("conditionpercentage", 100.0f) / 100.0f * item.MaxCondition; + } + else + { + //backwards compatibility + item.condition = element.GetAttributeFloat("condition", item.condition); + //if the item was in full condition considering the unmodified health + //(not taking possible HealthMultipliers added by mods into account), + //make sure it stays in full condition + if (item.condition > 0) + { + bool wasFullCondition = prevCondition >= item.Prefab.Health; + if (wasFullCondition) + { + item.condition = item.MaxCondition; + } + item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); + } + } + item.lastSentCondition = item.condition; + item.RecalculateConditionValues(); + item.SetActiveSprite(); + foreach (ItemComponent component in item.components) { + if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } @@ -3419,11 +3503,6 @@ namespace Barotrauma element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier)))); } - if (condition < MaxCondition) - { - element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture))); - } - if (!MathUtils.NearlyEqual(healthMultiplier, 1.0f)) { element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture))); @@ -3460,6 +3539,16 @@ namespace Barotrauma upgrade.Save(element); } + if (condition < MaxCondition) + { + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); + } + else + { + var conditionAttribute = element.GetAttribute("condition"); + if (conditionAttribute != null) { conditionAttribute.Remove(); } + } + parentElement.Add(element); return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index e0c65a747..77b03826f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -18,9 +19,10 @@ namespace Barotrauma AssignCampaignInteraction = 6, ApplyStatusEffect = 7, Upgrade = 8, + ItemStat = 9, MinValue = 0, - MaxValue = 6 + MaxValue = 9 } public interface IEventData : NetEntityEvent.IData @@ -56,10 +58,24 @@ namespace Barotrauma { public EventType EventType => EventType.ChangeProperty; public readonly SerializableProperty SerializableProperty; + public readonly ISerializableEntity Entity; - public ChangePropertyEventData(SerializableProperty serializableProperty) + public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity) { SerializableProperty = serializableProperty; + Entity = entity; + } + } + + public readonly struct SetItemStatEventData : IEventData + { + public EventType EventType => EventType.ItemStat; + + public readonly Dictionary Stats; + + public SetItemStatEventData(Dictionary stats) + { + Stats = stats; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 9dd19dc7f..83d4a3788 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -47,8 +47,8 @@ namespace Barotrauma CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", - element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]); - RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); + element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); + RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty()); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); @@ -102,12 +102,13 @@ namespace Barotrauma { public readonly Identifier ItemPrefabIdentifier; - public ItemPrefab ItemPrefab => ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab - : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab ?? throw new Exception($"No ItemPrefab with identifier or name \"{ItemPrefabIdentifier}\""); + public ItemPrefab ItemPrefab => + ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab + : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab; public override UInt32 UintIdentifier { get; } - public override IEnumerable ItemPrefabs => ItemPrefab.ToEnumerable(); + public override IEnumerable ItemPrefabs => ItemPrefab == null ? Enumerable.Empty() : ItemPrefab.ToEnumerable(); public override ItemPrefab FirstMatchingPrefab => ItemPrefab; @@ -122,6 +123,11 @@ namespace Barotrauma using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); } + + public override string ToString() + { + return $"{base.ToString()} ({ItemPrefabIdentifier})"; + } } public class RequiredItemByTag : RequiredItem @@ -146,6 +152,11 @@ namespace Barotrauma using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); } + + public override string ToString() + { + return $"{base.ToString()} ({Tag})"; + } } public readonly Identifier TargetItemPrefabIdentifier; @@ -412,7 +423,6 @@ namespace Barotrauma public ImmutableArray Triggers { get; private set; } private ImmutableDictionary treatmentSuitability; - private readonly List fabricationRecipeElements = new List(); /// /// Is this prefab overriding a prefab in another content package @@ -754,6 +764,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DontTransferBetweenSubs { get; private set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowHealthBar { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -801,8 +814,6 @@ namespace Barotrauma ? category : MapEntityCategory.Misc; - var parentType = ConfigElement.Parent?.GetAttributeIdentifier("itemtype", ""); - //nameidentifier can be used to make multiple items use the same names and descriptions Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", ""); @@ -818,7 +829,7 @@ namespace Barotrauma name = name.Fallback(OriginalName); } - if (parentType == "wrecked") + if (category == MapEntityCategory.Wrecked) { name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } @@ -1145,7 +1156,7 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return priceInfo != null && priceInfo.CanBeBought && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } public bool CanBeBoughtFrom(Location location) @@ -1156,7 +1167,7 @@ namespace Barotrauma var priceInfo = GetPriceInfo(store.Value); if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } - if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; } + if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; } return true; } return false; @@ -1242,13 +1253,12 @@ namespace Barotrauma throw new ArgumentException("Both name and identifier cannot be null."); } - ItemPrefab prefab; if (identifier.IsEmpty) { //legacy support identifier = GenerateLegacyIdentifier(name); } - Prefabs.TryGet(identifier, out prefab); + Prefabs.TryGet(identifier, out ItemPrefab prefab); //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1296,8 +1306,8 @@ namespace Barotrauma return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags)); } - private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; - private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => + private static bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; + private static bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item)); public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs new file mode 100644 index 000000000..c6091915f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -0,0 +1,61 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal sealed class ItemStatManager + { + private Item item; + + public ItemStatManager(Item item) + { + this.item = item; + } + + [NetworkSerialize] + public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct; + + private readonly Dictionary talentStats = new(); + + public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent) + { + if (talent.Character?.ID is not { } characterId || + talent.Prefab?.Identifier is not { } talentIdentifier) + { + return; + } + + TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId); + talentStats[identifier] = value; + +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(item, new Item.SetItemStatEventData(talentStats)); + } +#endif + } + + // Used for getting the value value from network packet + public void ApplyStat(TalentStatIdentifier identifier, float value) + { + talentStats[identifier] = value; + } + + public float GetAdjustedValue(ItemTalentStats stat, float originalValue) + { + float total = originalValue; + foreach (var (key, value) in talentStats) + { + if (key.Stat == stat) + { + total *= value; + } + } + + return total; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 04351e385..41e4f333c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Microsoft.Xna.Framework; +using Barotrauma.Extensions; namespace Barotrauma { @@ -21,9 +23,11 @@ namespace Barotrauma public bool MatchOnEmpty { get; set; } + public bool RequireEmpty { get; set; } + public bool IgnoreInEditor { get; set; } - private ImmutableHashSet excludedIdentifiers; + public ImmutableHashSet ExcludedIdentifiers { get; private set; } private RelationType type; @@ -54,6 +58,20 @@ namespace Barotrauma /// public int TargetSlot = -1; + /// + /// Overrides the position defined in ItemContainer. + /// + public Vector2? ItemPos; + + /// + /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. + /// + public bool? Hide; + + public float Rotation; + + public bool SetActive; + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -69,20 +87,20 @@ namespace Barotrauma public string JoinedExcludedIdentifiers { - get { return string.Join(",", excludedIdentifiers); } + get { return string.Join(",", ExcludedIdentifiers); } set { if (value == null) return; - excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet(); + ExcludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet(); } } public bool MatchesItem(Item item) { if (item == null) { return false; } - if (excludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; } - foreach (var excludedIdentifier in excludedIdentifiers) + if (ExcludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; } + foreach (var excludedIdentifier in ExcludedIdentifiers) { if (item.HasTag(excludedIdentifier)) { return false; } } @@ -100,8 +118,8 @@ namespace Barotrauma public bool MatchesItem(ItemPrefab itemPrefab) { if (itemPrefab == null) { return false; } - if (excludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; } - foreach (var excludedIdentifier in excludedIdentifiers) + if (ExcludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; } + foreach (var excludedIdentifier in ExcludedIdentifiers) { if (itemPrefab.Tags.Contains(excludedIdentifier)) { return false; } } @@ -120,7 +138,7 @@ namespace Barotrauma public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers) { this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); - this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); + this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); statusEffects = new List(); } @@ -133,40 +151,52 @@ namespace Barotrauma if (parentItem == null) { return false; } return CheckContained(parentItem); case RelationType.Container: - if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } - return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && (!ExcludeFullCondition || !parentItem.Container.IsFullCondition) && MatchesItem(parentItem.Container); + if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty || RequireEmpty; } + return CheckItem(parentItem.Container, this); case RelationType.Equipped: if (character == null) { return false; } - if (MatchOnEmpty && !character.HeldItems.Any()) { return true; } - foreach (Item equippedItem in character.HeldItems) + var heldItems = character.HeldItems; + if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; } + foreach (Item equippedItem in heldItems) { if (equippedItem == null) { continue; } - if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && (!ExcludeFullCondition || !equippedItem.IsFullCondition) && MatchesItem(equippedItem)) { return true; } + if (CheckItem(equippedItem, this)) + { + if (RequireEmpty && equippedItem.Condition > 0) { return false; } + return true; + } } break; case RelationType.Picked: - if (character == null || character.Inventory == null) { return false; } - foreach (Item pickedItem in character.Inventory.AllItems) + if (character == null) { return false; } + if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; } + var allItems = character.Inventory.AllItems; + if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; } + foreach (Item pickedItem in allItems) { - if (MatchesItem(pickedItem)) { return true; } + if (pickedItem == null) { continue; } + if (CheckItem(pickedItem, this)) + { + if (RequireEmpty && pickedItem.Condition > 0) { return false; } + return true; + } } break; default: return true; } + static bool CheckItem(Item i, RelatedItem ri) => (!ri.ExcludeBroken || ri.RequireEmpty || i.Condition > 0.0f) && (!ri.ExcludeFullCondition || !i.IsFullCondition) && ri.MatchesItem(i); + return false; } private bool CheckContained(Item parentItem) { if (parentItem.OwnInventory == null) { return false; } - - if (MatchOnEmpty && parentItem.OwnInventory.IsEmpty()) - { - return true; - } - + bool isEmpty = parentItem.OwnInventory.IsEmpty(); + if (RequireEmpty && !isEmpty) { return false; } + if (MatchOnEmpty && isEmpty) { return true; } foreach (Item contained in parentItem.ContainedItems) { if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } @@ -184,11 +214,23 @@ namespace Barotrauma new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("requireempty", RequireEmpty), new XAttribute("excludefullcondition", ExcludeFullCondition), new XAttribute("targetslot", TargetSlot), - new XAttribute("allowvariants", AllowVariants)); + new XAttribute("allowvariants", AllowVariants), + new XAttribute("rotation", Rotation), + new XAttribute("setactive", SetActive)); - if (excludedIdentifiers.Count > 0) + if (Hide.HasValue) + { + element.Add(new XAttribute(nameof(Hide), Hide.Value)); + } + if (ItemPos.HasValue) + { + element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value)); + } + + if (ExcludedIdentifiers.Count > 0) { element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers)); } @@ -249,9 +291,20 @@ namespace Barotrauma RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) { ExcludeBroken = element.GetAttributeBool("excludebroken", true), + RequireEmpty = element.GetAttributeBool("requireempty", false), ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), - AllowVariants = element.GetAttributeBool("allowvariants", true) + AllowVariants = element.GetAttributeBool("allowvariants", true), + Rotation = element.GetAttributeFloat("rotation", 0f), + SetActive = element.GetAttributeBool("setactive", false) }; + if (element.GetAttribute(nameof(Hide)) != null) + { + ri.Hide = element.GetAttributeBool(nameof(Hide), false); + } + if (element.GetAttribute(nameof(ItemPos)) != null) + { + ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); + } string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 8eabe4b03..0770ba69f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -1026,7 +1026,7 @@ namespace Barotrauma.MapCreatures.Behavior branch.DamageVisualizationTimer = 1.0f; } - if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } + if (branch.IsRootGrowth && root is { Health: > 0.0f }) { return; } if (type != AttackType.Other && type != AttackType.CutFromRoot) { @@ -1035,7 +1035,7 @@ namespace Barotrauma.MapCreatures.Behavior } if (GameMain.NetworkMember != null) - { + { // damage is handled server side if (GameMain.NetworkMember.IsClient) { @@ -1059,6 +1059,11 @@ namespace Barotrauma.MapCreatures.Behavior if (type == AttackType.Fire) { + if (attacker is not null) + { + damage *= 1f + attacker.GetStatValue(StatTypes.BallastFloraDamageMultiplier); + } + if (IsInWater(branch)) { damage *= 1f - SubmergedWaterResistance; @@ -1066,7 +1071,7 @@ namespace Barotrauma.MapCreatures.Behavior if (defenseCooldown <= 0) { - if (!(StateMachine.State is DefendWithPumpState)) + if (StateMachine.State is not DefendWithPumpState) { StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); defenseCooldown = 180f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 329102f51..c05c9517a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -1,13 +1,12 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -28,13 +27,14 @@ namespace Barotrauma private readonly bool applyFireEffects; private readonly string[] ignoreFireEffectsForTags; private readonly bool ignoreCover; - private readonly bool onlyInside,onlyOutside; private readonly float flashDuration; private readonly float? flashRange; private readonly string decal; private readonly float decalSize; private readonly bool applyToSelf; + public bool OnlyInside, OnlyOutside; + private readonly float itemRepairStrength; public readonly HashSet IgnoredSubmarines = new HashSet(); @@ -82,8 +82,8 @@ namespace Barotrauma ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty(), convertToLowerInvariant: true); ignoreCover = element.GetAttributeBool("ignorecover", false); - onlyInside = element.GetAttributeBool("onlyinside", false); - onlyOutside = element.GetAttributeBool("onlyoutside", false); + OnlyInside = element.GetAttributeBool("onlyinside", false); + OnlyOutside = element.GetAttributeBool("onlyoutside", false); flash = element.GetAttributeBool("flash", showEffects); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); @@ -131,17 +131,23 @@ namespace Barotrauma if (damageSource is Item sourceItem) { var launcher = sourceItem.GetComponent()?.Launcher; - displayRange *= - 1.0f - + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + displayRange *= + 1.0f + + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + (launcher?.GetQualityModifier(Quality.StatType.ExplosionRadius) ?? 0); - Attack.DamageMultiplier *= - 1.0f + Attack.DamageMultiplier *= + 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage) + (launcher?.GetQualityModifier(Quality.StatType.ExplosionDamage) ?? 0); Attack.SourceItem ??= sourceItem; } + if (attacker is not null) + { + displayRange *= 1f + attacker.GetStatValue(StatTypes.ExplosionRadiusMultiplier); + Attack.DamageMultiplier *= 1f + attacker.GetStatValue(StatTypes.ExplosionDamageMultiplier); + } + Vector2 cameraPos = GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f); @@ -171,13 +177,12 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); - if (distSqr > displayRangeSqr) continue; - - float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + if (distSqr > displayRangeSqr) { continue; } + float distFactor = CalculateDistanceFactor(distSqr, displayRange); //damage repairable power-consuming items var powered = item.GetComponent(); - if (powered == null || !powered.VulnerableToEMP) continue; + if (powered == null || !powered.VulnerableToEMP) { continue; } if (item.Repairables.Any()) { item.Condition -= item.MaxCondition * EmpStrength * distFactor; @@ -187,9 +192,10 @@ namespace Barotrauma var powerContainer = item.GetComponent(); if (powerContainer != null) { - powerContainer.Charge -= powerContainer.Capacity * EmpStrength * distFactor; + powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } + static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange; } if (itemRepairStrength > 0.0f) @@ -283,10 +289,16 @@ namespace Barotrauma { continue; } - if (c == attacker && !applyToSelf) { continue; } + //if (c == attacker && !applyToSelf) { continue; } - if (onlyInside && c.Submarine == null) { continue; } - else if (onlyOutside && c.Submarine != null) { continue; } + if (OnlyInside && c.Submarine == null) + { + continue; + } + else if (OnlyOutside && c.Submarine != null) + { + continue; + } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } @@ -332,15 +344,19 @@ namespace Barotrauma modifiedAfflictions.Clear(); foreach (Affliction affliction in attack.Afflictions.Keys) { - // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor - // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. - float limbCountFactor = Math.Min(distFactors.Count, 15); float dmgMultiplier = distFactor; if (affliction.DivideByLimbCount) { + float limbCountFactor = distFactors.Count; + if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage") + { + // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor + // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. + limbCountFactor = Math.Min(distFactors.Count, 15); + } dmgMultiplier /= limbCountFactor; } - modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability)); + modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction)); } c.LastDamageSource = damageSource; if (attacker == null) @@ -348,26 +364,29 @@ namespace Barotrauma if (damageSource is Item item) { attacker = item.GetComponent()?.User; - if (attacker == null) - { - attacker = item.GetComponent()?.User; - } + attacker ??= item.GetComponent()?.User; } } - AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); - if (attackData.Afflictions != null) + if (attack.Afflictions.Any() || attack.Stun > 0.0f) { - modifiedAfflictions.AddRange(attackData.Afflictions); + if (!attack.OnlyHumans || c.IsHuman) + { + AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); + if (attackData.Afflictions != null) + { + modifiedAfflictions.AddRange(attackData.Afflictions); + } + + //use a position slightly from the limb's position towards the explosion + //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods + Vector2 dir = worldPosition - limb.WorldPosition; + Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); + damages.Add(limb, attackResult.Damage); + } } - //use a position slightly from the limb's position towards the explosion - //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods - Vector2 dir = worldPosition - limb.WorldPosition; - Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); - damages.Add(limb, attackResult.Damage); - if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); @@ -430,7 +449,7 @@ namespace Barotrauma damagedStructureList.Clear(); foreach (MapEntity entity in MapEntity.mapEntityList) { - if (!(entity is Structure structure)) { continue; } + if (entity is not Structure structure) { continue; } if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; } if (structure.HasBody && @@ -479,7 +498,7 @@ namespace Barotrauma for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) { - if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; } + if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; } foreach (var cell in destructibleWall.Cells) { if (cell.IsPointInside(worldPosition)) @@ -502,7 +521,7 @@ namespace Barotrauma return damagedStructures; } - public void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) + public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) { List ballastFlorae = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index fb0b0c1ee..86f06c14f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -550,21 +550,24 @@ namespace Barotrauma if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface < rect.Y) { + //create a wave from the side of the hull the water is leaking from if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f; - vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - - hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime; - hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime; + CreateWave(rect, hull1, hull1.WaveY.Length - 1, hull1.WaveY.Length - 2, flowForce, deltaTime); } else { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f; + CreateWave(rect, hull1, 0, 1, flowForce, deltaTime); + } + static void CreateWave(Rectangle rect, Hull hull1, int index1, int index2, Vector2 flowForce, float deltaTime) + { + float vel = (rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[index1]); vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - - hull1.WaveVel[0] += vel * deltaTime; - hull1.WaveVel[1] += vel * deltaTime; + if (vel > 0.0f) + { + hull1.WaveVel[index1] += vel * deltaTime; + hull1.WaveVel[index2] += vel * deltaTime; + } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index ee915e10e..1144fc52b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,3 +1,5 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; using System.Collections.Immutable; namespace Barotrauma @@ -18,6 +20,14 @@ namespace Barotrauma public readonly ImmutableHashSet AllowedZones; + private readonly SubmarineAvailability? submarineAvailability; + private readonly ImmutableHashSet submarineAvailabilityOverrides; + + public readonly record struct SubmarineAvailability( + Identifier LocationType, + SubmarineClass Class = SubmarineClass.Undefined, + int MaxTier = 0); + public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element)) { OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); @@ -34,6 +44,26 @@ namespace Barotrauma AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); + + var submarineAvailabilityOverrides = new HashSet(); + if (element.GetChildElement("submarines") is ContentXElement availabilityElement) + { + submarineAvailability = GetAvailability(availabilityElement); + foreach (var overrideElement in availabilityElement.GetChildElements("override")) + { + var availabilityOverride = GetAvailability(overrideElement); + submarineAvailabilityOverrides.Add(availabilityOverride); + } + } + this.submarineAvailabilityOverrides = submarineAvailabilityOverrides.ToImmutableHashSet(); + + static SubmarineAvailability GetAvailability(ContentXElement element) + { + return new SubmarineAvailability( + LocationType: element.GetAttributeIdentifier("locationtype", Identifier.Empty), + Class: element.GetAttributeEnum("class", SubmarineClass.Undefined), + MaxTier: element.GetAttributeInt("maxtier", 0)); + } } public static Identifier ParseIdentifier(ContentXElement element) @@ -47,6 +77,31 @@ namespace Barotrauma return identifier; } + public int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType) + { + if (!submarineAvailability.HasValue) + { + // If the availability is not explicitly defined, make all subs available + return SubmarineInfo.HighestTier; + } + int maxTier = submarineAvailability.Value.MaxTier; + if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == subClass) is SubmarineAvailability locationAndClassOverride) + { + maxTier = locationAndClassOverride.MaxTier; + } + else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == SubmarineClass.Undefined) is SubmarineAvailability locationOverride) + { + maxTier = locationOverride.MaxTier; + } + else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == Identifier.Empty && a.Class == subClass) is SubmarineAvailability classOverride) + { + maxTier = classOverride.MaxTier; + } + return maxTier; + } + + public bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType) => info.Tier <= HighestSubmarineTierAvailable(info.SubmarineClass, locationType); + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 4daa4282a..d4c14c59e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -228,6 +228,9 @@ namespace Barotrauma continue; } + Vector2 edgeDiff = edge.Point2 - edge.Point1; + Vector2 edgeDir = Vector2.Normalize(edgeDiff); + //If the edge is next to an empty cell and there's another solid cell at the other side of the empty one, //don't touch this edge. Otherwise we may end up closing off small passages between cells. var adjacentEmptyCell = edge.AdjacentCell(cell); @@ -238,8 +241,10 @@ namespace Barotrauma //find the edge at the opposite side of the adjacent cell foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges) { - if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) > 0 && - otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) + if (otherEdge == edge || otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) { continue; } + Vector2 otherEdgeDir = Vector2.Normalize(otherEdge.Point2 - otherEdge.Point1); + //dot product is > 0.7 if the edges are roughly parallel + if (Math.Abs(Vector2.Dot(otherEdgeDir, edgeDir)) > 0.7f) { adjacentEdge = otherEdge; break; @@ -251,13 +256,11 @@ namespace Barotrauma continue; } } - List edgePoints = new List(); Vector2 edgeNormal = edge.GetNormal(cell); float edgeLength = Vector2.Distance(edge.Point1, edge.Point2); int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1); - Vector2 edgeDir = edge.Point2 - edge.Point1; for (int i = 0; i <= pointCount; i++) { if (i == 0) @@ -275,7 +278,7 @@ namespace Barotrauma float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient); Vector2 extrudedPoint = edge.Point1 + - edgeDir * (i / (float)pointCount) + + edgeDiff * (i / (float)pointCount) + edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF; var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 1665d6a7f..eec99f20b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -447,7 +447,7 @@ namespace Barotrauma private Level(LevelData levelData) : base(null, 0) { - this.LevelData = levelData; + LevelData = levelData; borders = new Rectangle(Point.Zero, levelData.Size); } @@ -709,7 +709,7 @@ namespace Barotrauma if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } } - if (!TooClose(siteX, siteY)) + if (!TooCloseToOtherSites(siteX, siteY)) { siteCoordsX.Add(siteX); siteCoordsY.Add(siteY); @@ -717,14 +717,14 @@ namespace Barotrauma if (closeToCave) { - for (int x2 = x; x2 < x + siteInterval.X; x2 += caveSiteInterval) + for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval) { - for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) + for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) { int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); - if (!TooClose(caveSiteX, caveSiteY)) + if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval)) { siteCoordsX.Add(caveSiteX); siteCoordsY.Add(caveSiteY); @@ -735,11 +735,12 @@ namespace Barotrauma } } - bool TooClose(double siteX, double siteY) + bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f) { + float minDistanceSqr = minDistance * minDistance; for (int i = 0; i < siteCoordsX.Count; i++) { - if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < 10.0f * 10.0f) + if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr) { return true; } @@ -2539,7 +2540,8 @@ namespace Barotrauma levelResources.Add((itemPrefab, commonnessInfo)); } else if (itemPrefab.LevelQuantity.TryGetValue(GenerationParams.Identifier, out var fixedQuantityResourceInfo) || - itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) + itemPrefab.LevelQuantity.TryGetValue(LevelData.Biome.Identifier, out fixedQuantityResourceInfo) || + itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) { fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); } @@ -3939,34 +3941,14 @@ namespace Barotrauma } SubmarineInfo outpostInfo; - Submarine outpost; + Submarine outpost = null; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { - if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null) + if (LevelData.OutpostGenerationParamsExist) { Location location = i == 0 ? StartLocation : EndLocation; - - OutpostGenerationParams outpostGenerationParams = null; - if (LevelData.ForceOutpostGenerationParams != null) - { - outpostGenerationParams = LevelData.ForceOutpostGenerationParams; - } - else - { - var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); - if (!suitableParams.Any()) - { - suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); - if (!suitableParams.Any()) - { - DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); - suitableParams = OutpostGenerationParams.OutpostParams; - } - } - - outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.ServerAndClient); - } - + OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ?? + LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient); LocationType locationType = location?.Type; if (locationType == null) { @@ -4324,6 +4306,10 @@ namespace Barotrauma sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); // Deduce the job from the selected prefab selectedPrefab = GetCorpsePrefab(usedJobs); + if (selectedPrefab != null) + { + job = selectedPrefab.GetJobPrefab(); + } } } if (selectedPrefab == null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 4120c334a..1ac0bfd73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -57,9 +57,19 @@ namespace Barotrauma /// public int? MinMainPathWidth; + /// + /// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of . + /// public readonly List EventHistory = new List(); + + /// + /// Events that have already triggered in this level and can never trigger again. . + /// public readonly List NonRepeatableEvents = new List(); + /// + /// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. . + /// public bool EventsExhausted { get; set; } /// @@ -146,7 +156,6 @@ namespace Barotrauma EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } - /// /// Instantiates level data using the properties of the connection (seed, size, difficulty) /// @@ -243,6 +252,23 @@ namespace Barotrauma return levelData; } + public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any(); + + public static IEnumerable GetSuitableOutpostGenerationParams(Location location) + { + var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); + if (!suitableParams.Any()) + { + suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); + if (!suitableParams.Any()) + { + DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); + suitableParams = OutpostGenerationParams.OutpostParams; + } + } + return suitableParams; + } + public void Save(XElement parentElement) { var newElement = new XElement("Level", @@ -284,6 +310,7 @@ namespace Barotrauma newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier)))); } } + parentElement.Add(newElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 8e30125de..5b9d587fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -542,38 +542,41 @@ namespace Barotrauma GlobalForceDecreaseTimer = 0.0f; } - foreach (LevelObject obj in updateableObjects) + if (updateableObjects is not null) { - if (GameMain.NetworkMember is { IsServer: true }) + foreach (LevelObject obj in updateableObjects) { - obj.NetworkUpdateTimer -= deltaTime; - if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); - obj.NeedsNetworkSyncing = false; - obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; - } - } - if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - - if (obj.Triggers != null) - { - obj.ActivePrefab = obj.Prefab; - for (int i = 0; i < obj.Triggers.Count; i++) - { - obj.Triggers[i].Update(deltaTime); - if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + obj.NetworkUpdateTimer -= deltaTime; + if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) { - obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); + obj.NeedsNetworkSyncing = false; + obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } } - } + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - if (obj.PhysicsBody != null) - { - if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } - /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); - obj.Rotation = -obj.PhysicsBody.Rotation;*/ + if (obj.Triggers != null) + { + obj.ActivePrefab = obj.Prefab; + for (int i = 0; i < obj.Triggers.Count; i++) + { + obj.Triggers[i].Update(deltaTime); + if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + { + obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + } + } + } + + if (obj.PhysicsBody != null) + { + if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } + /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); + obj.Rotation = -obj.PhysicsBody.Rotation;*/ + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index a2ad86140..213fad7bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -287,6 +287,13 @@ namespace Barotrauma private set; } + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] + public Color SpriteColor + { + get; + private set; + } + public string Name => Identifier.Value; public List ChildObjects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 480163ffa..a5c1b3f85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -661,7 +661,7 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effect.type, deltaTime, triggerer, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 52bc59f55..636c47c1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -405,7 +405,7 @@ namespace Barotrauma if (wall.Submarine != sub) { continue; } for (int i = 0; i < wall.SectionCount; i++) { - wall.SetDamage(i, 0, createNetworkEvent: false); + wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false); } } foreach (Hull hull in Hull.HullList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 2efccc7e5..19224427b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -61,7 +61,7 @@ namespace Barotrauma private LocationType addInitialMissionsForType; - public bool Discovered { get; private set; } + public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; @@ -135,7 +135,7 @@ namespace Barotrauma foreach (var stockElement in storeElement.GetChildElements("stock")) { var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty); - if (identifier.IsEmpty || !(ItemPrefab.FindByIdentifier(identifier) is ItemPrefab prefab)) { continue; } + if (identifier.IsEmpty || ItemPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { continue; } int qty = stockElement.GetAttributeInt("qty", 0); if (qty < 1) { continue; } Stock.Add(new PurchasedItem(prefab, qty, buyer: null)); @@ -157,7 +157,7 @@ namespace Barotrauma foreach (var childElement in element.GetChildElements("item")) { var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); - if (id.IsEmpty || !(ItemPrefab.FindByIdentifier(id) is ItemPrefab prefab)) { continue; } + if (id.IsEmpty || ItemPrefab.FindByIdentifier(id) is not ItemPrefab prefab) { continue; } specials.Add(prefab); } return specials; @@ -240,7 +240,7 @@ namespace Barotrauma availableStock.Add(stockItem.ItemPrefab, weight); } DailySpecials.Clear(); - int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount(); + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++) { if (availableStock.None()) { break; } @@ -283,6 +283,17 @@ namespace Barotrauma } // Adjust by current location reputation price *= Location.GetStoreReputationModifier(true); + + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (characters.Any()) + { + if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) + { + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated)); + } + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); + price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); + } // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -303,6 +314,14 @@ namespace Barotrauma } // Adjust by current location reputation price *= Location.GetStoreReputationModifier(false); + + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + 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.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag))); + } + // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -478,7 +497,6 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - Discovered = element.GetAttributeBool("discovered", false); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); @@ -641,7 +659,7 @@ namespace Barotrauma return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } - public void ChangeType(LocationType newType) + public void ChangeType(LocationType newType, bool createStores = true) { if (newType == Type) { return; } @@ -665,7 +683,10 @@ namespace Barotrauma UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); } - CreateStores(force: true); + if (createStores) + { + CreateStores(force: true); + } } public void UnlockInitialMissions() @@ -1125,7 +1146,7 @@ namespace Barotrauma public void UpdateStores() { // In multiplayer, stores should be updated by the server and loaded from save data by clients - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } if (Stores == null) { CreateStores(); @@ -1167,13 +1188,10 @@ namespace Barotrauma stockToRemove.ForEach(i => stock.Remove(i)); store.Stock.Clear(); store.Stock.AddRange(stock); - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || store.DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) - { - store.GenerateSpecials(); - } store.GeneratePriceModifier(); } + + StepsSinceSpecialsUpdated++; foreach (var identifier in storesToRemove) { Stores.Remove(identifier); @@ -1184,6 +1202,20 @@ namespace Barotrauma } } + public void UpdateSpecials() + { + if (GameMain.NetworkMember is { IsClient: true } || Stores is null) { return; } + + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + + foreach (StoreInfo store in Stores.Values) + { + if (StepsSinceSpecialsUpdated < SpecialsUpdateInterval && store.DailySpecials.Count == DailySpecialsCount + extraSpecialSalesCount) { continue; } + + store.GenerateSpecials(); + } + } + private void UpdateStoreIdentifiers() { StoreIdentifiers.Clear(); @@ -1255,21 +1287,37 @@ namespace Barotrauma } } - public int GetExtraSpecialSalesCount() + public static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (!characters.Any()) { return 0; } - return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); + return characters.Max(static c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - public void Discover(bool checkTalents = true) + public bool CanHaveSubsForSale() { - if (Discovered) { return; } - Discovered = true; - if (checkTalents) + return HasOutpost() && CanHaveCampaignInteraction(CampaignMode.InteractionType.PurchaseSub); + } + + public int HighestSubmarineTierAvailable(SubmarineClass submarineClass = SubmarineClass.Undefined) + { + if (CanHaveSubsForSale()) { - GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); + return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier; } + return 0; + } + + public bool IsSubmarineAvailable(SubmarineInfo info) + { + return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true; + } + + private bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType) + { + return LevelData != null && + LevelData.OutpostGenerationParamsExist && + LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType)); } public void Reset() @@ -1283,7 +1331,6 @@ namespace Barotrauma ClearMissions(); LevelData?.EventHistory?.Clear(); UnlockInitialMissions(); - Discovered = false; } public XElement Save(Map map, XElement parentElement) @@ -1294,7 +1341,6 @@ namespace Barotrauma new XAttribute("basename", BaseName), new XAttribute("name", Name), new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), - new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), @@ -1423,7 +1469,7 @@ namespace Barotrauma HireManager?.Remove(); } - class AbilityLocation : AbilityObject, IAbilityLocation + public class AbilityLocation : AbilityObject, IAbilityLocation { public AbilityLocation(Location location) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 97651462a..2a97aa299 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -24,6 +24,7 @@ namespace Barotrauma public readonly Dictionary MinCountPerZone = new Dictionary(); public readonly LocalizedString Name; + public readonly LocalizedString Description; public readonly float BeaconStationChance; @@ -70,6 +71,13 @@ namespace Barotrauma public Sprite Sprite { get; private set; } public Sprite RadiationSprite { get; } + private readonly Identifier forceOutpostGenerationParamsIdentifier; + + /// + /// If set to true, only event sets that explicitly define this location type in can be selected at this location. Defaults to false. + /// + public bool IgnoreGenericEvents { get; } + public Color SpriteColor { get; @@ -77,9 +85,9 @@ namespace Barotrauma } public float StoreMaxReputationModifier { get; } = 0.1f; - public float StoreSellPriceModifier { get; } = 0.8f; + public float StoreSellPriceModifier { get; } = 0.3f; public float DailySpecialPriceModifier { get; } = 0.5f; - public float RequestGoodPriceModifier { get; } = 1.5f; + public float RequestGoodPriceModifier { get; } = 2f; public int StoreInitialBalance { get; } = 5000; /// /// In percentages @@ -96,6 +104,7 @@ namespace Barotrauma public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { Name = TextManager.Get("LocationName." + Identifier, "unknown"); + Description = TextManager.Get("LocationDescription." + Identifier, ""); BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); @@ -110,6 +119,10 @@ namespace Barotrauma ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty); + forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty); + + IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false); + string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); @@ -261,6 +274,15 @@ namespace Barotrauma } } + public OutpostGenerationParams GetForcedOutpostGenerationParams() + { + if (OutpostGenerationParams.OutpostParams.TryGet(forceOutpostGenerationParamsIdentifier, out var parameters)) + { + return parameters; + } + return null; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index e87fd88da..0d1956a5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -68,10 +68,15 @@ namespace Barotrauma public List Locations { get; private set; } + private readonly List locationsDiscovered = new List(); + private readonly List outpostsVisited = new List(); + public List Connections { get; private set; } public Radiation Radiation; + private bool wasLocationDiscoveryOrderTracked = true; + public Map(CampaignSettings settings) { generationParams = MapGenerationParams.Instance; @@ -282,7 +287,12 @@ namespace Barotrauma } } - CurrentLocation.Discover(true); + if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost)) + { + CurrentLocation.ChangeType(tutorialOutpost); + } + Discover(CurrentLocation); + Visit(CurrentLocation); CurrentLocation.CreateStores(); foreach (var location in Locations) @@ -542,7 +552,8 @@ namespace Barotrauma Connections[i].Locations[1]; if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); + leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + createStores: false); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; @@ -618,6 +629,7 @@ namespace Barotrauma foreach (Location location in Locations) { location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.CreateStores(force: true); } foreach (LocationConnection connection in Connections) { @@ -724,7 +736,7 @@ namespace Barotrauma if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) { - previousToEndLocation.ChangeType(locationType); + previousToEndLocation.ChangeType(locationType, createStores: false); } //remove all locations from the end biome except the end location @@ -820,7 +832,8 @@ namespace Barotrauma SelectedConnection.Passed = true; CurrentLocation = SelectedLocation; - CurrentLocation.Discover(); + Discover(CurrentLocation); + Visit(CurrentLocation); SelectedLocation = null; CurrentLocation.CreateStores(); @@ -851,7 +864,7 @@ namespace Barotrauma Location prevLocation = CurrentLocation; CurrentLocation = Locations[index]; - CurrentLocation.Discover(); + Discover(CurrentLocation); CurrentLocation.CreateStores(); if (prevLocation != CurrentLocation) @@ -982,6 +995,16 @@ namespace Barotrauma ProgressWorld(); } + // always update specials every step + for (int i = 0; i < Math.Max(1, steps); i++) + { + foreach (Location location in Locations) + { + if (!location.Discovered) { continue; } + location.UpdateSpecials(); + } + } + Radiation?.OnStep(steps); } @@ -1174,6 +1197,51 @@ namespace Barotrauma partial void ClearAnimQueue(); + public void Discover(Location location, bool checkTalents = true) + { + if (location is null) { return; } + if (locationsDiscovered.Contains(location)) { return; } + locationsDiscovered.Add(location); + if (checkTalents) + { + GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Location.AbilityLocation(location))); + } + } + + public void Visit(Location location) + { + if (location is null) { return; } + if (!location.HasOutpost()) { return; } + if (outpostsVisited.Contains(location)) { return; } + outpostsVisited.Add(location); + } + + public void ClearLocationHistory() + { + locationsDiscovered.Clear(); + outpostsVisited.Clear(); + } + + public int? GetDiscoveryIndex(Location location) + { + if (!wasLocationDiscoveryOrderTracked) { return null; } + if (location is null) { return -1; } + return locationsDiscovered.IndexOf(location); + } + + public int? GetVisitIndex(Location location) + { + if (!wasLocationDiscoveryOrderTracked) { return null; } + if (location is null) { return -1; } + return outpostsVisited.IndexOf(location); + } + + public bool IsDiscovered(Location location) + { + if (location is null) { return false; } + return locationsDiscovered.Contains(location); + } + /// /// Load a previously saved map from an xml element /// @@ -1201,6 +1269,7 @@ namespace Barotrauma return; } + ClearLocationHistory(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1216,19 +1285,12 @@ namespace Barotrauma } } location.LoadLocationTypeChange(subElement); + + // Backwards compatibility if (subElement.GetAttributeBool("discovered", false)) { - location.Discover(checkTalents: false); - } - if (location.Discovered) - { -#if CLIENT - RemoveFogOfWar(location); -#endif - if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) - { - furthestDiscoveredLocation = location; - } + Discover(location); + wasLocationDiscoveryOrderTracked = false; } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); @@ -1258,6 +1320,36 @@ namespace Barotrauma case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; + case "discovered": + foreach (var childElement in subElement.GetChildElements("location")) + { + int index = childElement.GetAttributeInt("i", -1); + if (index < 0) { continue; } + if (Locations[index] is not Location l) { continue; } + Discover(l); + } + break; + case "visited": + foreach (var childElement in subElement.GetChildElements("location")) + { + int index = childElement.GetAttributeInt("i", -1); + if (index < 0) { continue; } + if (Locations[index] is not Location l) { continue; } + Visit(l); + } + break; + } + } + + void Discover(Location location) + { + this.Discover(location, checkTalents: false); +#if CLIENT + RemoveFogOfWar(location); +#endif + if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + { + furthestDiscoveredLocation = location; } } @@ -1333,6 +1425,30 @@ namespace Barotrauma mapElement.Add(Radiation.Save()); } + if (locationsDiscovered.Any()) + { + var discoveryElement = new XElement("discovered"); + foreach (Location location in locationsDiscovered) + { + int index = Locations.IndexOf(location); + var locationElement = new XElement("location", new XAttribute("i", index)); + discoveryElement.Add(locationElement); + } + mapElement.Add(discoveryElement); + } + + if (outpostsVisited.Any()) + { + var visitElement = new XElement("visited"); + foreach (Location location in outpostsVisited) + { + int index = Locations.IndexOf(location); + var locationElement = new XElement("location", new XAttribute("i", index)); + visitElement.Add(locationElement); + } + mapElement.Add(visitElement); + } + element.Add(mapElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index bdec8d1b4..6d9077cc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -10,6 +10,7 @@ namespace Barotrauma [Flags] enum MapEntityCategory { + None = 0, Structure = 1, Decorative = 2, Machine = 4, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 6e9b95be9..f91f5e1a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -96,6 +96,8 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } + public ContentPath OutpostFilePath { get; set; } + public class ModuleCount { public Identifier Identifier; @@ -182,6 +184,7 @@ namespace Barotrauma Name = element.GetAttributeString("name", Identifier.Value); allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath)); var humanPrefabCollections = new List>(); foreach (var subElement in element.Elements()) @@ -257,6 +260,21 @@ namespace Barotrauma return humanPrefabCollections.GetRandom(randSync); } + public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType) + { + foreach (var collection in humanPrefabCollections) + { + foreach (var prefab in collection) + { + if (prefab.CampaignInteractionType == interactionType) + { + return true; + } + } + } + return false; + } + public ImmutableHashSet GetStoreIdentifiers() { if (StoreIdentifiers == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 30217e2a5..9c76e0bb2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -143,7 +143,7 @@ namespace Barotrauma //select which module types the outpost should consist of List pendingModuleFlags = onlyEntrance ? - generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() : + (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : SelectModules(outpostModules, generationParams); foreach (Identifier flag in pendingModuleFlags) @@ -246,6 +246,7 @@ namespace Barotrauma var outpostFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) + .Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path)) .OrderBy(f => f.UintIdentifier).ToArray(); if (!outpostFiles.Any()) { @@ -696,6 +697,14 @@ namespace Barotrauma rect.Location += (module.Offset + module.MoveOffset).ToPoint(); rect.Y += module.Bounds.Height; + Vector2? selfGapPos1 = null; + Vector2? selfGapPos2 = null; + if (module.PreviousModule != null) + { + selfGapPos1 = module.Offset + module.ThisGap.Position + module.MoveOffset; + selfGapPos2 = module.PreviousModule.Offset + module.PreviousGap.Position + module.PreviousModule.MoveOffset; + } + foreach (PlacedModule otherModule in modules) { if (otherModule == module || otherModule.PreviousModule == null || otherModule.PreviousModule == module) { continue; } @@ -710,7 +719,17 @@ namespace Barotrauma Vector2 gapPos1 = otherModule.Offset + otherModule.ThisGap.Position + gapEdgeOffset + otherModule.MoveOffset; Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset; - if (Submarine.RectContains(rect, gapPos1) || Submarine.RectContains(rect, gapPos2) || MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _)) + if (Submarine.RectContains(rect, gapPos1) || + Submarine.RectContains(rect, gapPos2) || + MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _)) + { + return true; + } + + //check if the connection overlaps with this module's connection + if (selfGapPos1.HasValue && selfGapPos2.HasValue && + !gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) && + MathUtils.LinesIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 96a8e9028..532ce3957 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -27,6 +27,8 @@ namespace Barotrauma public bool DisplayNonEmpty { get; } = false; public Identifier StoreIdentifier { get; } + public bool RequiresUnlock { get; } + /// /// Used when both and are set to 0. /// @@ -48,11 +50,12 @@ namespace Barotrauma int maxAmount = GetMaxAmount(element); maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); + RequiresUnlock = element.GetAttributeBool("requiresunlock", false); } public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, - bool displayNonEmpty = false, string storeIdentifier = null) + bool displayNonEmpty = false, bool requiresUnlock = false, string storeIdentifier = null) { Price = price; CanBeBought = canBeBought; @@ -64,6 +67,7 @@ namespace Barotrauma CanBeSpecial = canBeSpecial; DisplayNonEmpty = displayNonEmpty; StoreIdentifier = new Identifier(storeIdentifier); + RequiresUnlock = requiresUnlock; } public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) @@ -78,6 +82,7 @@ namespace Barotrauma float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true)); + bool requiresUnlock = element.GetAttributeBool("requiresunlock", false); foreach (XElement childElement in element.GetChildElements("price")) { float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); @@ -91,26 +96,28 @@ namespace Barotrauma } string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier); // TODO: Add some error messages if we have defined the min or max amount while the item is not sold - var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), - sold, - sold ? GetMinAmount(childElement, minAmount) : 0, - sold ? GetMaxAmount(childElement, maxAmount) : 0, - canBeSpecial, - storeMinLevelDifficulty, - storeBuyingMultiplier, - displayNonEmpty, - storeIdentifier); + var priceInfo = new PriceInfo(price: (int)(priceMultiplier * basePrice), + canBeBought: sold, + minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, + maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, + canBeSpecial: canBeSpecial, + minLevelDifficulty: storeMinLevelDifficulty, + buyingPriceMultiplier: storeBuyingMultiplier, + displayNonEmpty: displayNonEmpty, + requiresUnlock: requiresUnlock, + storeIdentifier: storeIdentifier); priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); - defaultPrice = new PriceInfo(basePrice, - soldElsewhere, - soldElsewhere ? minAmount : 0, - soldElsewhere ? maxAmount : 0, - canBeSpecial, - minLevelDifficulty, - buyingPriceMultiplier, - displayNonEmpty); + defaultPrice = new PriceInfo(price: basePrice, + canBeBought: soldElsewhere, + minAmount: soldElsewhere ? minAmount : 0, + maxAmount: soldElsewhere ? maxAmount : 0, + canBeSpecial: canBeSpecial, + minLevelDifficulty: minLevelDifficulty, + buyingPriceMultiplier: buyingPriceMultiplier, + displayNonEmpty: displayNonEmpty, + requiresUnlock: requiresUnlock); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 36d7e96a5..8cd78e3f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -56,6 +56,8 @@ namespace Barotrauma //dimensions of the wall sections' physics bodies (only used for debug rendering) private readonly List bodyDebugDimensions = new List(); + private static Explosion explosionOnBroken; + #if DEBUG [Serialize(false, IsPropertySaveable.Yes), Editable] #else @@ -1083,7 +1085,7 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true) { if (Submarine != null && Submarine.GodMode || Indestructible) { return; } if (!Prefab.Body) { return; } @@ -1128,6 +1130,7 @@ namespace Barotrauma } else { + float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f; if (Sections[sectionIndex].gap == null) { Rectangle gapRect = Sections[sectionIndex].rect; @@ -1204,8 +1207,15 @@ namespace Barotrauma #endif } + var gap = Sections[sectionIndex].gap; float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); - Sections[sectionIndex].gap.Open = gapOpen; + gap.Open = gapOpen; + + //gap appeared or became much larger -> explosion effect + if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom) + { + CreateWallDamageExplosion(gap, attacker); + } } float damageDiff = damage - Sections[sectionIndex].damage; @@ -1234,6 +1244,59 @@ namespace Barotrauma UpdateSections(); } + private void CreateWallDamageExplosion(Gap gap, Character attacker) + { + const float explosionRange = 750.0f; + float explosionStrength = gap.Open; + + var linkedHull = gap.linkedTo.FirstOrDefault() as Hull; + if (linkedHull != null) + { + //existing, nearby gaps leading to the same hull reduce the strength of the explosion + // -> the first breached section does most (or all) of the damage, making it more consistent + // (otherwise the damage would depend on how many structures and sections happen to be breached) + foreach (var otherGap in linkedHull.ConnectedGaps) + { + if (otherGap == gap || otherGap.IsRoomToRoom || otherGap.Open < 0.25f) { continue; } + explosionStrength -= Math.Max(0, explosionRange - Vector2.Distance(otherGap.WorldPosition, gap.WorldPosition)) / explosionRange; + if (explosionStrength <= 0.0f) { return; } + } + } + + if (explosionOnBroken == null) + { + explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); + if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations)) + { + explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null); + } + else + { + explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); + } + explosionOnBroken.OnlyInside = true; + explosionOnBroken.DisableParticles(); + } + + explosionOnBroken.Attack.DamageMultiplier = explosionStrength; + explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker); +#if CLIENT + if (linkedHull != null) + { + for (int i = 0; i <= 50; i++) + { + Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y)); + var velocity = gap.IsHorizontal ? + gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX : + gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY; + velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f)); + var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f); + if (particle == null) { break; } + } + } +#endif + } + partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount); public void SetCollisionCategory(Category collisionCategory) @@ -1570,7 +1633,7 @@ namespace Barotrauma { for (int i = 0; i < Sections.Length; i++) { - SetDamage(i, Sections[i].damage, createNetworkEvent: false); + SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 5ae404712..e2958efdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1114,7 +1114,8 @@ namespace Barotrauma { if (item.Submarine != this) { continue; } var pump = item.GetComponent(); - if (pump == null || !item.HasTag("ballast") || item.CurrentHull == null) { continue; } + if (pump == null || item.CurrentHull == null) { continue; } + if (!item.HasTag("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 b9af39add..fa7470faa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -720,7 +720,7 @@ namespace Barotrauma private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { - if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10) + if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10) { //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 426941fe8..c40ed58c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -314,6 +314,7 @@ namespace Barotrauma Tier = original.Tier; IsManuallyOutfitted = original.IsManuallyOutfitted; Tags = original.Tags; + OutpostGenerationParams = original.OutpostGenerationParams; if (original.OutpostModuleInfo != null) { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); @@ -747,6 +748,8 @@ namespace Barotrauma return doc; } - public static int GetDefaultTier(int price) => price > 20000 ? 3 : price > 10000 ? 2 : 1; + public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1; + + public const int HighestTier = 3; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index f88aeb8f3..acb7bee9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -159,7 +159,11 @@ namespace Barotrauma.Networking return command; } - public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionmult = 2.0f) + /// + /// How much messages sent by should get garbled. Takes the distance between the entities and optionally the obstructions between them into account (see ). + /// + /// Values greater than or equal to 1 cause the message to get garbled more heavily when there's some obstruction between the characters. Values smaller than 1 mean the garbling only depends on distance. + public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionMultiplier = 2.0f) { if (listener == null || sender == null) { @@ -172,12 +176,12 @@ namespace Barotrauma.Networking Hull listenerHull = listener == null ? null : Hull.FindHull(listener.WorldPosition); Hull sourceHull = sender == null ? null : Hull.FindHull(sender.WorldPosition); - if (sourceHull != listenerHull) + if (sourceHull != listenerHull && obstructionMultiplier >= 1.0f) { if ((sourceHull == null || !sourceHull.GetConnectedHulls(includingThis: false, searchDepth: 2, ignoreClosedGaps: true).Contains(listenerHull)) && Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null) { - dist = (dist + 100f) * obstructionmult; + dist = (dist + 100f) * obstructionMultiplier; } } if (dist > range) { return 1.0f; } @@ -192,9 +196,9 @@ namespace Barotrauma.Networking return ApplyDistanceEffect(listener, Sender, Text, SpeakRange); } - public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionmult = 2.0f) + public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionMultiplier = 2.0f) { - return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionmult)); + return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionMultiplier)); } public static string ApplyDistanceEffect(string text, float garbleAmount) @@ -247,7 +251,7 @@ namespace Barotrauma.Networking var senderRadio = senderItem.GetComponent(); if (!receiverRadio.CanReceive(senderRadio)) { continue; } - string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range); + string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range, obstructionMultiplier: 0); if (sender.SpeechImpediment > 0.0f) { //speech impediment doesn't reduce the range when using a radio, but adds extra garbling diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index cd1b6ad8a..4f8157b45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -23,13 +23,22 @@ namespace Barotrauma.Networking { Success = 0x00, Heartbeat = 0x01, + RequestShutdown = 0xCC, Crash = 0xFF } private static ManualResetEvent writeManualResetEvent; - private static volatile bool shutDown; - public static bool HasShutDown => shutDown; + private enum StatusEnum + { + NeverStarted, + Active, + RequestedShutDown, + ShutDown + } + + private static volatile StatusEnum status = StatusEnum.NeverStarted; + public static bool HasShutDown => status is StatusEnum.ShutDown; private const int ReadBufferSize = MsgConstants.MTU * 2; private static byte[] readTempBytes; @@ -38,7 +47,7 @@ namespace Barotrauma.Networking private static ConcurrentQueue msgsToWrite; private static ConcurrentQueue errorsToWrite; - + private static ConcurrentQueue msgsToRead; private static Thread readThread; @@ -48,6 +57,8 @@ namespace Barotrauma.Networking private static void PrivateStart() { + status = StatusEnum.Active; + readIncOffset = 0; readIncTotal = 0; @@ -58,8 +69,6 @@ namespace Barotrauma.Networking msgsToRead = new ConcurrentQueue(); - shutDown = false; - readCancellationToken = new CancellationTokenSource(); writeManualResetEvent = new ManualResetEvent(false); @@ -80,7 +89,13 @@ namespace Barotrauma.Networking private static void PrivateShutDown() { - shutDown = true; + if (Thread.CurrentThread != GameMain.MainThread) + { + throw new InvalidOperationException( + $"Cannot call {nameof(ChildServerRelay)}.{nameof(PrivateShutDown)} from a thread other than the main one"); + } + if (status is StatusEnum.NeverStarted) { return; } + status = StatusEnum.ShutDown; writeManualResetEvent?.Set(); readCancellationToken?.Cancel(); readThread?.Join(); readThread = null; @@ -93,18 +108,18 @@ namespace Barotrauma.Networking } - private static int ReadIncomingMsgs() + private static Option ReadIncomingMsgs() { Task readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token); - if (readTask is null) { return -1; } + if (readTask is null) { return Option.None(); } int timeOutMilliseconds = 100; for (int i = 0; i < 150; i++) { - if (shutDown) + if (status is StatusEnum.ShutDown) { readCancellationToken?.Cancel(); - return -1; + return Option.None(); } try { @@ -115,36 +130,36 @@ namespace Barotrauma.Networking } catch (AggregateException aggregateException) { - if (aggregateException.InnerException is OperationCanceledException) { return -1; } + if (aggregateException.InnerException is OperationCanceledException) { return Option.None(); } throw; } catch (OperationCanceledException) { - return -1; + return Option.None(); } } - if (readTask.Status != TaskStatus.RanToCompletion) + if (readTask.Status == TaskStatus.RanToCompletion) { - bool swallowException = shutDown - && ((readTask.Exception?.InnerException is ObjectDisposedException) - || (readTask.Exception?.InnerException is System.IO.IOException)); - if (swallowException) - { - readCancellationToken?.Cancel(); - return -1; - } - throw new Exception( - $"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.", - readTask.Exception); + return Option.Some(readTask.Result); } - return readTask.Result; + bool swallowException = + status is not StatusEnum.Active + && readTask.Exception?.InnerException is ObjectDisposedException or System.IO.IOException; + if (swallowException) + { + readCancellationToken?.Cancel(); + return Option.None(); + } + throw new Exception( + $"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.", + readTask.Exception); } private static void CheckPipeConnected(string name, PipeType pipe) { - if (!(pipe is { IsConnected: true })) + if (status is StatusEnum.Active && pipe is not { IsConnected: true }) { throw new Exception($"{name} was disconnected unexpectedly"); } @@ -155,7 +170,7 @@ namespace Barotrauma.Networking private static void UpdateRead() { Span msgLengthSpan = stackalloc byte[4 + 1]; - while (!shutDown) + while (!HasShutDown) { CheckPipeConnected(nameof(readStream), readStream); @@ -165,10 +180,9 @@ namespace Barotrauma.Networking { if (readIncOffset >= readIncTotal) { - readIncTotal = ReadIncomingMsgs(); + if (!ReadIncomingMsgs().TryUnwrap(out readIncTotal)) { return false; } readIncOffset = 0; if (readIncTotal == 0) { Thread.Yield(); continue; } - if (readIncTotal < 0) { return false; } } readTo[i] = readTempBytes[readIncOffset]; readIncOffset++; @@ -176,7 +190,7 @@ namespace Barotrauma.Networking return true; } - if (!readBytes(msgLengthSpan)) { shutDown = true; break; } + if (!readBytes(msgLengthSpan)) { status = StatusEnum.ShutDown; break; } int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8) @@ -184,24 +198,24 @@ namespace Barotrauma.Networking | (msgLengthSpan[3] << 24); WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4]; - if (msgLength > 0) - { - byte[] msg = new byte[msgLength]; - if (!readBytes(msg.AsSpan())) { shutDown = true; break; } + byte[] msg = msgLength > 0 ? new byte[msgLength] : Array.Empty(); + if (msg.Length > 0 && !readBytes(msg.AsSpan())) { status = StatusEnum.ShutDown; break; } - switch (writeStatus) - { - case WriteStatus.Success: - msgsToRead.Enqueue(msg); - break; - case WriteStatus.Heartbeat: - //do nothing - break; - case WriteStatus.Crash: - HandleCrashString(Encoding.UTF8.GetString(msg)); - shutDown = true; - break; - } + switch (writeStatus) + { + case WriteStatus.Success: + msgsToRead.Enqueue(msg); + break; + case WriteStatus.Heartbeat: + //do nothing + break; + case WriteStatus.RequestShutdown: + status = StatusEnum.ShutDown; + break; + case WriteStatus.Crash: + HandleCrashString(Encoding.UTF8.GetString(msg)); + status = StatusEnum.ShutDown; + break; } Thread.Yield(); @@ -210,13 +224,11 @@ namespace Barotrauma.Networking private static void UpdateWrite() { - while (!shutDown) + while (!HasShutDown) { CheckPipeConnected(nameof(writeStream), writeStream); - byte[] msg; - - void writeMsg(WriteStatus writeStatus) + void writeMsg(WriteStatus writeStatus, byte[] msg) { // It's SUPER IMPORTANT that this stack allocation // remains in this local function and is never inlined, @@ -224,21 +236,19 @@ namespace Barotrauma.Networking // when the function returns; placing it in the loop // this method is based around would lead to a stack // overflow real quick! - Span bytesToWrite = stackalloc byte[4 + 1 + msg.Length]; + Span headerBytes = stackalloc byte[4 + 1]; - bytesToWrite[0] = (byte)(msg.Length & 0xFF); - bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); - bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF); - bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF); + headerBytes[0] = (byte)(msg.Length & 0xFF); + headerBytes[1] = (byte)((msg.Length >> 8) & 0xFF); + headerBytes[2] = (byte)((msg.Length >> 16) & 0xFF); + headerBytes[3] = (byte)((msg.Length >> 24) & 0xFF); - bytesToWrite[4] = (byte)writeStatus; - Span msgSlice = bytesToWrite.Slice(4 + 1, msg.Length); - - msg.AsSpan().CopyTo(msgSlice); + headerBytes[4] = (byte)writeStatus; try { - writeStream?.Write(bytesToWrite); + writeStream?.Write(headerBytes); + writeStream?.Write(msg); } catch (Exception exception) { @@ -246,7 +256,7 @@ namespace Barotrauma.Networking { case ObjectDisposedException _: case System.IO.IOException _: - if (!shutDown) { throw; } + if (!HasShutDown) { throw; } break; default: throw; @@ -254,29 +264,34 @@ namespace Barotrauma.Networking } } + if (status is StatusEnum.RequestedShutDown) + { + writeMsg(WriteStatus.RequestShutdown, Array.Empty()); + status = StatusEnum.ShutDown; + } + while (errorsToWrite.TryDequeue(out var error)) { - msg = Encoding.UTF8.GetBytes(error); - writeMsg(WriteStatus.Crash); - shutDown = true; + writeMsg(WriteStatus.Crash, Encoding.UTF8.GetBytes(error)); + status = StatusEnum.ShutDown; } - while (msgsToWrite.TryDequeue(out msg)) + while (msgsToWrite.TryDequeue(out var msg)) { - writeMsg(WriteStatus.Success); + writeMsg(WriteStatus.Success, msg); - if (shutDown) { break; } + if (HasShutDown) { break; } } - if (!shutDown) + if (!HasShutDown) { writeManualResetEvent.Reset(); if (!writeManualResetEvent.WaitOne(1000)) { - if (shutDown) { return; } + if (HasShutDown) { return; } //heartbeat to keep the other end alive - msg = Array.Empty(); writeMsg(WriteStatus.Heartbeat); + writeMsg(WriteStatus.Heartbeat, Array.Empty()); } } } @@ -284,7 +299,7 @@ namespace Barotrauma.Networking public static void Write(byte[] msg) { - if (shutDown) { return; } + if (HasShutDown) { return; } if (msg.Length > 0x1fff_ffff) { @@ -298,7 +313,7 @@ namespace Barotrauma.Networking public static bool Read(out byte[] msg) { - if (shutDown) { msg = null; return false; } + if (HasShutDown) { msg = null; return false; } return msgsToRead.TryDequeue(out msg); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 6e5300c4c..8bde51456 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Networking SellSubItems = 0x4000, ManageMap = 0x8000, ManageHires = 0x10000, - All = 0x1FFFF + ManageBotTalents = 0x20000, + All = 0x3FFFF } class PermissionPreset diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 79fb09932..872daa00e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -40,15 +40,15 @@ namespace Barotrauma.Networking LUA_NET_MESSAGE } - enum ClientNetObject + + enum ClientNetSegment { - END_OF_MESSAGE, //self-explanatory - SYNC_IDS, //ids of the last changes the client knows about - CHAT_MESSAGE, //also self-explanatory - VOTE, //you get the idea - CHARACTER_INPUT, - ENTITY_STATE, - SPECTATING_POS + SyncIds, //ids of the last changes the client knows about + ChatMessage, //also self-explanatory + Vote, //you get the idea + CharacterInput, + EntityState, + SpectatingPos } enum ClientNetError @@ -92,16 +92,15 @@ namespace Barotrauma.Networking LUA_NET_MESSAGE } - enum ServerNetObject + enum ServerNetSegment { - END_OF_MESSAGE, - SYNC_IDS, - CHAT_MESSAGE, - VOTE, - CLIENT_LIST, - ENTITY_POSITION, - ENTITY_EVENT, - ENTITY_EVENT_INITIAL + SyncIds, + ChatMessage, + Vote, + ClientList, + EntityPosition, + EntityEvent, + EntityEventInitial } enum TraitorMessageType diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs index eaea9b556..bb8eeaa40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs @@ -17,14 +17,9 @@ namespace Barotrauma.Networking public LidgrenAddress(IPAddress netAddress) { - if (IPAddress.IsLoopback(netAddress)) - { - NetAddress = IPAddress.Loopback; - } - else - { - NetAddress = netAddress; - } + if (IPAddress.IsLoopback(netAddress)) { netAddress = IPAddress.Loopback; } + if (netAddress.IsIPv4MappedToIPv6) { netAddress = netAddress.MapToIPv4(); } + NetAddress = netAddress; } public new static Option Parse(string endpointStr) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs index 8e11264c6..6fc7a7c8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Networking public LidgrenEndpoint(IPEndPoint netEndpoint) : base(new LidgrenAddress(netEndpoint.Address)) { - NetEndpoint = netEndpoint; + NetEndpoint = new IPEndPoint((Address as LidgrenAddress)!.NetAddress, netEndpoint.Port); } public new static Option Parse(string endpointStr) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 2ef9e10ec..bcc5c90b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -272,6 +272,9 @@ namespace Barotrauma.Networking [NetworkSerialize] public bool IsMandatory; + [NetworkSerialize] + public bool IsVanilla; + private Md5Hash? cachedHash; private DateTime? cachedDateTime; @@ -305,6 +308,7 @@ namespace Barotrauma.Networking ? ugcId.StringRepresentation : ""; IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile); + IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage; InstallTimeDiffInSeconds = contentPackage.InstallTime.TryUnwrap(out var installTime) ? (uint)(installTime - referenceTime).TotalSeconds diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 1c0680fbd..0ec3e3c2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Networking public State CurrentState { get; private set; } - public bool UseRespawnPrompt + public static bool UseRespawnPrompt { get { @@ -199,6 +199,7 @@ namespace Barotrauma.Networking public void ForceRespawn() { ResetShuttle(); + RespawnCountdownStarted = true; RespawnTime = DateTime.Now; CurrentState = State.Waiting; } @@ -274,7 +275,7 @@ namespace Barotrauma.Networking var powerContainer = item.GetComponent(); if (powerContainer != null) { - powerContainer.Charge = powerContainer.Capacity; + powerContainer.Charge = powerContainer.GetCapacity(); } var door = item.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 9b05b697e..8132ffd76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -386,12 +386,17 @@ namespace Barotrauma.Networking private bool autoRestart; - public bool IsPublic; - private int maxPlayers; public List ClientPermissions { get; private set; } = new List(); + [Serialize(true, IsPropertySaveable.Yes)] + public bool IsPublic + { + get; + set; + } + private int tickRate = 20; [Serialize(20, IsPropertySaveable.Yes)] public int TickRate @@ -522,13 +527,20 @@ namespace Barotrauma.Networking } } - [Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)] + [Serialize(LosMode.Opaque, IsPropertySaveable.Yes)] public LosMode LosMode { get; set; } + [Serialize(EnemyHealthBarMode.ShowAll, IsPropertySaveable.Yes)] + public EnemyHealthBarMode ShowEnemyHealthBars + { + get; + set; + } + [Serialize(800, IsPropertySaveable.Yes)] public int LinesPerLogFile { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index ee9831e4d..8286eee68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -97,7 +97,6 @@ namespace Barotrauma get { return list; } } - protected Vector2 prevPosition; protected float prevRotation; @@ -344,6 +343,22 @@ namespace Barotrauma } } + /// + /// Ignore rotation calls for the rest of this and the next update. Automatically disabled after that. Used for temporarily suppressing the SmoothRotate calls to prevent conflicting or unitentionally amplified rotations. + /// + public bool SuppressSmoothRotationCalls + { + get => _suppressSmoothRotationCalls; + set + { + _suppressSmoothRotationCalls = value; + smoothRotationSuppressionCounter = 0; + } + } + + private bool _suppressSmoothRotationCalls; + private int smoothRotationSuppressionCounter; + public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { } public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { } public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { } @@ -831,6 +846,17 @@ namespace Barotrauma } drawOffset = NetConfig.InterpolateSimPositionError(drawOffset, PositionSmoothingFactor); rotationOffset = NetConfig.InterpolateRotationError(rotationOffset); + if (SuppressSmoothRotationCalls) + { + if (smoothRotationSuppressionCounter > 0) + { + SuppressSmoothRotationCalls = false; + } + else + { + smoothRotationSuppressionCounter++; + } + } } public void UpdateDrawPosition() @@ -873,6 +899,7 @@ namespace Barotrauma /// Should the angles be wrapped. Set to false if it makes a difference whether the angle of the body is 0.0f or 360.0f. public void SmoothRotate(float targetRotation, float force = 10.0f, bool wrapAngle = true) { + if (SuppressSmoothRotationCalls) { return; } float nextAngle = FarseerBody.Rotation + FarseerBody.AngularVelocity * (float)Timing.Step; float angle = wrapAngle ? MathUtils.GetShortestAngle(nextAngle, targetRotation) : @@ -881,7 +908,7 @@ namespace Barotrauma if (FarseerBody.BodyType == BodyType.Kinematic) { - if (!IsValidValue(torque, "torque")) return; + if (!IsValidValue(torque, "torque")) { return; } FarseerBody.AngularVelocity = torque; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 93fa4d6a7..4b3b7edc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; @@ -256,7 +257,7 @@ namespace Barotrauma /// Prefab identifier /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not - public bool TryGet(Identifier identifier, out T? result) + public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) { Prefab.DisallowCallFromConstructor(); if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 2b8e658f9..226e1c3c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -65,7 +65,7 @@ namespace Barotrauma } #endif #if CLIENT - (botCountText as GUITextBlock).Text = botCount.ToString(); + botCountText.Text = botCount.ToString(); #endif } @@ -79,7 +79,13 @@ namespace Barotrauma } #endif #if CLIENT - (botSpawnModeText as GUITextBlock).Text = TextManager.Get(botSpawnMode.ToString()); + + botSpawnModeText.Text = TextManager.Get(botSpawnMode.ToString()); + botSpawnModeText.ToolTip = TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip") + "\n\n" + TextManager.Get("botspawn.campaignnote"); + foreach (var btn in botSpawnModeButtons) + { + btn.ToolTip = botSpawnModeText.ToolTip; + } #endif } @@ -89,7 +95,7 @@ namespace Barotrauma if (GameMain.Server != null) GameMain.Server.ServerSettings.TraitorsEnabled = enabled; #endif #if CLIENT - (traitorProbabilityText as GUITextBlock).Text = TextManager.Get(enabled.ToString()); + traitorProbabilityText.Text = TextManager.Get(enabled.ToString()); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs index 86cce0a43..622a47591 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs @@ -35,6 +35,7 @@ { GUI.DisableSavingIndicatorDelayed(); } + GameMain.ResetIMEWorkaround(); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 16e5022f4..09e78f943 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -214,6 +214,7 @@ namespace Barotrauma return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) { return element.GetAttributeStringArray(name, null, trim: trim, convertToLowerInvariant: false) @@ -221,6 +222,12 @@ namespace Barotrauma ?? defaultValue; } + public static ImmutableHashSet GetAttributeIdentifierImmutableHashSet(this XElement element, string key, ImmutableHashSet defaultValue, bool trim = true) + { + return element.GetAttributeIdentifierArray(key, null, trim)?.ToImmutableHashSet() + ?? defaultValue; + } + public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { if (element == null) { return defaultValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index e1ec4e598..b327c3903 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -35,10 +35,19 @@ namespace Barotrauma Activity } + public enum EnemyHealthBarMode + { + ShowAll, + BossHealthBarsOnly, + HideAll + } + public static class GameSettings { public struct Config { + public const float DefaultAimAssist = 0.05f; + public static Config GetDefault() { Config config = new Config @@ -50,7 +59,8 @@ namespace Barotrauma SubEditorBackground = new Color(13, 37, 69, 255), EnableSplashScreen = true, PauseOnFocusLost = true, - AimAssistAmount = 0.5f, + AimAssistAmount = DefaultAimAssist, + ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, @@ -110,6 +120,7 @@ namespace Barotrauma public LanguageIdentifier Language; public bool VerboseLogging; public bool SaveDebugConsoleLogs; + public string SavePath; public int SubEditorUndoBuffer; public int MaxAutoSaves; public int AutoSaveIntervalSeconds; @@ -118,6 +129,7 @@ namespace Barotrauma public bool PauseOnFocusLost; public float AimAssistAmount; public bool EnableMouseLook; + public EnemyHealthBarMode ShowEnemyHealthBars; public bool ChatOpen; public bool CrewMenuOpen; public bool EditorDisclaimerShown; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 6435eebf4..60b0aca6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -95,11 +95,7 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (this.type != type) { return; } - if (intervalTimer > 0.0f) - { - intervalTimer -= deltaTime; - return; - } + if (ShouldWaitForInterval(entity, deltaTime)) { return; } if (!HasRequiredItems(entity)) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } if (!Stackable) diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index f86226e55..301137e0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -24,7 +24,8 @@ namespace Barotrauma HasSpecifierTag, Affliction, EntityType, - LimbType + LimbType, + SkillRequirement } public enum Comparison @@ -73,6 +74,7 @@ namespace Barotrauma case "targetcontainer": case "targetgrandparent": case "targetcontaineditem": + case "skillrequirement": return false; default: return true; @@ -110,6 +112,11 @@ namespace Barotrauma { Type = ConditionType.Uncertain; } + + if (attribute.Parent.GetAttributeBool("skillrequirement", false)) + { + Type = ConditionType.SkillRequirement; + } AttributeValue = valueString; SplitAttributeValue = valueString.Split(','); @@ -305,25 +312,20 @@ namespace Barotrauma if (health == null) { return false; } var affliction = health.GetAffliction(AttributeName.ToIdentifier()); float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - if (FloatValue.HasValue) - { - float value = FloatValue.Value; - switch (Operator) - { - case OperatorType.Equals: - return afflictionStrength == value; - case OperatorType.GreaterThan: - return afflictionStrength > value; - case OperatorType.GreaterThanEquals: - return afflictionStrength >= value; - case OperatorType.LessThan: - return afflictionStrength < value; - case OperatorType.LessThanEquals: - return afflictionStrength <= value; - case OperatorType.NotEquals: - return afflictionStrength != value; - } - } + + return ValueMatchesRequirement(afflictionStrength); + } + } + return false; + case ConditionType.SkillRequirement: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + if (target is Character targetChar) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return ValueMatchesRequirement(skillLevel); } } return false; @@ -332,6 +334,30 @@ namespace Barotrauma } } + private bool ValueMatchesRequirement(float testedValue) + { + if (FloatValue.HasValue) + { + float value = FloatValue.Value; + switch (Operator) + { + case OperatorType.Equals: + return testedValue == value; + case OperatorType.GreaterThan: + return testedValue > value; + case OperatorType.GreaterThanEquals: + return testedValue >= value; + case OperatorType.LessThan: + return testedValue < value; + case OperatorType.LessThanEquals: + return testedValue <= value; + case OperatorType.NotEquals: + return testedValue != value; + } + } + return false; + } + private bool MatchesTagCondition(ISerializableEntity target) { if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } @@ -390,22 +416,26 @@ namespace Barotrauma return false; } - switch (Operator) { case OperatorType.Equals: - if (type == typeof(bool)) { - return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); + if (type == typeof(bool)) + { + return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); + } + var value = property.GetValue(target); + return Equals(value, AttributeValue); } - return property.GetValue(target).ToString().Equals(AttributeValue); - case OperatorType.NotEquals: - if (type == typeof(bool)) { - return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); + if (type == typeof(bool)) + { + return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); + } + var value = property.GetValue(target); + return !Equals(value, AttributeValue); } - return !property.GetValue(target).ToString().Equals(AttributeValue); case OperatorType.GreaterThan: case OperatorType.LessThanEquals: case OperatorType.LessThan: @@ -415,6 +445,18 @@ namespace Barotrauma break; } return false; + + static bool Equals(object value, string desiredValue) + { + if (value == null) + { + return desiredValue.Equals("null", StringComparison.OrdinalIgnoreCase); + } + else + { + return value.ToString().Equals(desiredValue); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index a17413b6f..e6b8c3779 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1,6 +1,7 @@ using Barotrauma.Abilities; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -245,11 +246,13 @@ namespace Barotrauma { public readonly Identifier SkillIdentifier; public readonly float Amount; + public readonly bool TriggerTalents; public GiveSkill(XElement element, string parentDebugName) { - SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty); - Amount = element.GetAttributeFloat("amount", 0); + SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty); + Amount = element.GetAttributeFloat(nameof(Amount), 0); + TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true); if (SkillIdentifier == Identifier.Empty) { @@ -278,6 +281,13 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.No)] public int Count { get; private set; } + /// + /// The maximum amount of creatures of the same species in the same team that are allowed to be spawned via this status effect. + /// Also the creatures spawned by other means are counted in the check. + /// + [Serialize(0, IsPropertySaveable.No)] + public int TotalMaxCount { get; private set; } + [Serialize(0, IsPropertySaveable.No)] public int Stun { get; private set; } @@ -335,7 +345,7 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; - public float intervalTimer; + public Dictionary intervalTimers = new Dictionary(); public static readonly List DurationList = new List(); @@ -362,7 +372,7 @@ namespace Barotrauma private readonly int useItemCount; - private readonly bool removeItem, dropContainedItems, removeCharacter, breakLimb, hideLimb; + private readonly bool removeItem, dropContainedItems, dropItem, removeCharacter, breakLimb, hideLimb; private readonly float hideLimbTimer; public readonly ActionType type = ActionType.OnActive; @@ -416,7 +426,7 @@ namespace Barotrauma private set; } - private readonly bool multiplyAfflictionsByMaxVitality; + private readonly bool? multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -484,7 +494,11 @@ namespace Barotrauma giveExperiences = new List(); giveSkills = new List(); luaHook = new List(); - multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); + var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); + if (multiplyAfflictionsElement != null) + { + multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); + } tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); @@ -658,6 +672,9 @@ namespace Barotrauma case "dropcontaineditems": dropContainedItems = true; break; + case "dropitem": + dropItem = true; + break; case "removecharacter": removeCharacter = true; break; @@ -871,10 +888,9 @@ namespace Barotrauma return true; } - public IReadOnlyList GetNearbyTargets(Vector2 worldPosition, List targets = null) + public void AddNearbyTargets(Vector2 worldPosition, List targets) { - targets ??= new List(); - if (Range <= 0.0f) { return targets; } + if (Range <= 0.0f) { return; } if (HasTargetType(TargetType.NearbyCharacters)) { foreach (Character c in Character.CharacterList) @@ -911,7 +927,6 @@ namespace Barotrauma } } } - return targets; bool CheckDistance(ISpatialEntity e) { @@ -1116,6 +1131,26 @@ namespace Barotrauma } } + private static readonly List intervalsToRemove = new List(); + public bool ShouldWaitForInterval(Entity entity, float deltaTime) + { + if (Interval > 0.0f && entity != null) + { + if (intervalTimers.ContainsKey(entity)) + { + intervalTimers[entity] -= deltaTime; + if (intervalTimers[entity] > 0.0f) { return true; } + } + intervalsToRemove.Clear(); + intervalsToRemove.AddRange(intervalTimers.Keys.Where(e => e.Removed)); + foreach (var toRemove in intervalsToRemove) + { + intervalTimers.Remove(toRemove); + } + } + return false; + } + public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) { return; } @@ -1143,12 +1178,7 @@ namespace Barotrauma public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (this.type != type) { return; } - - if (intervalTimer > 0.0f) - { - intervalTimer -= deltaTime; - return; - } + if (ShouldWaitForInterval(entity, deltaTime)) { return; } currentTargets.Clear(); foreach (ISerializableEntity target in targets) @@ -1251,12 +1281,7 @@ namespace Barotrauma lifeTimer -= deltaTime; if (lifeTimer <= 0) { return; } } - - if (intervalTimer > 0.0f) - { - intervalTimer -= deltaTime; - return; - } + if (ShouldWaitForInterval(entity, deltaTime)) { return; } { if (entity is Item item) @@ -1306,7 +1331,7 @@ namespace Barotrauma } for (int i = 0; i < targets.Count; i++) { - if (!(targets[i] is Item item)) { continue; } + if (targets[i] is not Item item) { continue; } for (int j = 0; j < useItemCount; j++) { if (item.Removed) { continue; } @@ -1315,6 +1340,16 @@ namespace Barotrauma } } + if (dropItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) + { + item.Drop(dropper: null); + } + } + } if (dropContainedItems) { for (int i = 0; i < targets.Count; i++) @@ -1329,6 +1364,13 @@ namespace Barotrauma } } } + else if (targets[i] is Character character && character.Inventory != null) + { + foreach (var containedItem in character.Inventory.AllItemsMod) + { + containedItem.Drop(dropper: null); + } + } } } if (removeItem) @@ -1366,17 +1408,20 @@ namespace Barotrauma if (limb.body == sourceBody) { targetLimb = limb; - if (breakLimb) - { - character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); - } break; } } } - if (hideLimb) + if (targetLimb != null) { - targetLimb?.HideAndDisable(hideLimbTimer); + if (breakLimb) + { + targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, ignoreSeveranceProbabilityModifier: true, attacker: user); + } + if (hideLimb) + { + targetLimb.HideAndDisable(hideLimbTimer); + } } } } @@ -1508,6 +1553,9 @@ namespace Barotrauma targetCharacter = targetLimb.character; } } + + Character entityCharacter = entity as Character; + targetCharacter ??= entityCharacter; if (targetCharacter != null && !targetCharacter.Removed && !targetCharacter.IsPlayer) { if (targetCharacter.AIController is EnemyAIController enemyAI) @@ -1515,7 +1563,13 @@ namespace Barotrauma foreach (AITrigger trigger in aiTriggers) { if (Rand.Value(Rand.RandSync.Unsynced) > trigger.Probability) { continue; } - if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb != targetLimb) { continue; } + if (entityCharacter != targetCharacter) + { + if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb is Limb hitLimb) + { + if (hitLimb != targetLimb) { continue; } + } + } if (targetCharacter.LastDamage.Damage < trigger.MinDamage) { continue; } enemyAI.LaunchTrigger(trigger); break; @@ -1556,9 +1610,7 @@ namespace Barotrauma if (targetCharacter != null && !targetCharacter.Removed) { Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; - - targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount); - + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents); Identifier GetRandomSkill() { return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty; @@ -1639,6 +1691,14 @@ namespace Barotrauma Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { + if (characterSpawnInfo.TotalMaxCount > 0) + { + if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) + { + Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); + return; + } + } if (newCharacter.AIController is EnemyAIController enemyAi && enemyAi.PetBehavior != null && entity is Item item && @@ -1699,11 +1759,11 @@ namespace Barotrauma Character.Controlled = newCharacter; } #elif SERVER - /*foreach (Client c in GameMain.Server.ConnectedClients) + foreach (Client c in GameMain.Server.ConnectedClients) { if (c.Character != target) { continue; } GameMain.Server.SetClientCharacter(c, newCharacter); - }*/ + } #endif } if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } @@ -1750,7 +1810,19 @@ namespace Barotrauma } float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); float rotation = chosenItemSpawnInfo.RotationRad; - Vector2 spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; + Vector2 worldPos; + if (sourceBody != null) + { + worldPos = sourceBody.Position; + if (user?.Submarine != null) + { + worldPos += user.Submarine.Position; + } + } + else + { + worldPos = entity.WorldPosition; + } switch (chosenItemSpawnInfo.RotationType) { case ItemSpawnInfo.SpawnRotationType.Fixed: @@ -1764,7 +1836,7 @@ namespace Barotrauma } break; case ItemSpawnInfo.SpawnRotationType.Target: - rotation = MathUtils.VectorToAngle(entity.SimPosition - spawnPos); + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); break; case ItemSpawnInfo.SpawnRotationType.Limb: if (sourceBody != null) @@ -1808,6 +1880,15 @@ namespace Barotrauma rotation += spread; if (projectile != null) { + Vector2 spawnPos; + if (projectile.Hitscan) + { + spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; + } + else + { + spawnPos = ConvertUnits.ToSimUnits(worldPos); + } projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); } @@ -1915,7 +1996,10 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); - intervalTimer = Interval; + if (Interval > 0.0f && entity != null) + { + intervalTimers[entity] = Interval; + } static Character CharacterFromTarget(ISerializableEntity target) { @@ -2088,26 +2172,39 @@ namespace Barotrauma if (entity is Item sourceItem && sourceItem.HasTag("medical")) { multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user != null) + + if (user is not null) { multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); } } + return multiplier * AfflictionMultiplier; } - private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality) + private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) { float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); - if (modifyByMaxVitality) + if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality) { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } + if (user is not null) + { + if (affliction.Prefab.IsBuff) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); + } + else if (affliction.Prefab.AfflictionType == "poison") + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); + } + } + if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { - return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability); + return affliction.CreateMultiplied(afflictionMultiplier, affliction); } return affliction; } @@ -2121,7 +2218,7 @@ namespace Barotrauma if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { - if (type == ActionType.OnUse) + if (type == ActionType.OnUse || type == ActionType.OnSuccess) { limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; } @@ -2156,4 +2253,4 @@ namespace Barotrauma return (tags.Contains(tag) || tags.Contains(tag.ToLowerInvariant())); } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index f8815e014..d9dd7a1eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Steam { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { - return new PublishedFileId[0]; + return Array.Empty(); } return Steamworks.SteamUGC.GetSubscribedItems(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index b4aa8a883..c9cde9d90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -61,6 +61,12 @@ namespace Barotrauma.Steam if (set.Count == prevSize) { break; } prevSize = set.Count; } + + // Remove items that do not have the correct consumer app ID, + // which can happen on items that are not visible to the currently + // logged in player (i.e. private & friends-only items) + set.RemoveWhere(it => it.ConsumerApp != AppID); + return set; } @@ -276,6 +282,62 @@ namespace Barotrauma.Steam } } } + + public static ISet GetInstalledItems() + => ContentPackageManager.WorkshopPackages + .Select(p => p.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value) + .ToHashSet(); + + public static async Task> GetPublishedAndSubscribedItems() + { + var allItems = (await GetAllSubscribedItems()).ToHashSet(); + allItems.UnionWith(await GetPublishedItems()); + + // This is a hack that eliminates subscribed mods that have been + // made private. Players cannot download updates for these, so + // we treat them as if they were deleted. + allItems = (await Task.WhenAll(allItems.Select(it => GetItem(it.Id.Value)))) + .NotNull() + .Where(it => it.ConsumerApp == AppID) + .ToHashSet(); + + return allItems; + } + + public static void DeleteUnsubscribedMods(Action? callback = null) + { +#if SERVER + // Servers do not run this because they can't subscribe to anything + return; +#endif + //If Steamworks isn't initialized then we can't know what the user has unsubscribed from + if (!IsInitialized) { return; } + if (!Steamworks.SteamClient.IsValid) { return; } + if (!Steamworks.SteamClient.IsLoggedOn) { return; } + + TaskPool.Add("DeleteUnsubscribedMods", GetPublishedAndSubscribedItems().WaitForLoadingScreen(), t => + { + if (!t.TryGetResult(out ISet items)) { return; } + var ids = items.Select(it => it.Id.Value).ToHashSet(); + var toUninstall = ContentPackageManager.WorkshopPackages + .Where(pkg + => !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId) + || !ids.Contains(workshopId.Value)) + .ToArray(); + if (toUninstall.Any()) + { + foreach (var pkg in toUninstall) + { + Directory.TryDelete(pkg.Dir, recursive: true); + } + ContentPackageManager.UpdateContentPackageList(); + } + callback?.Invoke(toUninstall); + }); + } public static bool IsInstallingToPath(string path) => File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName)); @@ -310,7 +372,7 @@ namespace Barotrauma.Steam + "unexpected deletion of your hard work.\n" + "Instead, modify a copy of your mod in LocalMods.\n"; - string workshopModDirReadmeLocation = Path.Combine(SaveUtil.SaveFolder, "WorkshopMods", "README.txt"); + string workshopModDirReadmeLocation = Path.Combine(SaveUtil.DefaultSaveFolder, "WorkshopMods", "README.txt"); if (!File.Exists(workshopModDirReadmeLocation)) { Directory.CreateDirectory(Path.GetDirectoryName(workshopModDirReadmeLocation)!); @@ -457,7 +519,7 @@ namespace Barotrauma.Steam } catch (Exception e) { - DebugConsole.ThrowError( + DebugConsole.AddWarning( $"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 9f3239e41..55e19dfad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace Barotrauma { @@ -44,8 +43,9 @@ namespace Barotrauma roundData = new RoundData(); foreach (Item item in Item.ItemList) { + if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; } Reactor reactor = item.GetComponent(); - if (reactor != null) { roundData.Reactors.Add(reactor); } + if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); } } pathFinder = new PathFinder(WayPoint.WayPointList, false); cachedDistances.Clear(); @@ -73,7 +73,7 @@ namespace Barotrauma { if (c.IsDead) { continue; } //achievement for descending below crush depth and coming back - if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) + if (GameMain.GameSession.RoundDuration > 30.0f) { if (c.Submarine != null && c.Submarine.AtDamageDepth || Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) > Level.Loaded.RealWorldCrushDepth) { @@ -97,7 +97,7 @@ namespace Barotrauma //get an achievement if they're still alive at the end of the round foreach (Character c in Character.CharacterList) { - if (!c.IsDead && c.Submarine == sub) roundData.ReactorMeltdown.Add(c); + if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); } } } } @@ -113,7 +113,7 @@ namespace Barotrauma //achievement for descending ridiculously deep float realWorldDepth = sub.RealWorldDepth; - if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) + if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f) { //all conscious characters inside the sub get an achievement UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); @@ -219,6 +219,12 @@ namespace Barotrauma UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } + public static void OnCampaignMetadataSet(Identifier identifier, object value) + { + if (identifier.IsEmpty || value is null) { return; } + UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier()); + } + public static void OnItemRepaired(Item item, Character fixer) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs index 7f27612ea..6ddc13c18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -13,7 +13,7 @@ namespace Barotrauma Yes } - protected LanguageIdentifier language { get; private set; } = LanguageIdentifier.None; + public LanguageIdentifier Language { get; private set; } = LanguageIdentifier.None; private int languageVersion = 0; protected string cachedValue = ""; @@ -32,13 +32,13 @@ namespace Barotrauma protected void UpdateLanguage() { - language = GameSettings.CurrentConfig.Language; + Language = GameSettings.CurrentConfig.Language; languageVersion = TextManager.LanguageVersion; } protected virtual bool MustRetrieveValue() //this can't be called on other LocalizedStrings by derived classes { - return language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; + return Language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; } protected static bool MustRetrieveValue(LocalizedString str) //this can be called by derived classes diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs index 0056fc9da..7c8ede811 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -54,10 +54,10 @@ namespace Barotrauma return (loaded ? candidates.GetRandomUnsynced() : "", loaded); } - var (value, loaded) = tryLoad(language); + var (value, loaded) = tryLoad(Language); loadedSuccessfully = loaded ? LoadedSuccessfully.Yes : LoadedSuccessfully.No; cachedValue = value; - if (!loaded && language != TextManager.DefaultLanguage) + if (!loaded && Language != TextManager.DefaultLanguage) { (value, _) = tryLoad(TextManager.DefaultLanguage); cachedValue = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 78cc321f7..53f0d75cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -1,12 +1,12 @@ #nullable enable using Barotrauma.IO; +using Barotrauma.Extensions; using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Linq; -using System.Text.RegularExpressions; using System.Xml.Linq; using System.Globalization; using System.Text.Unicode; @@ -20,6 +20,8 @@ namespace Barotrauma public static class TextManager { + public static bool DebugDraw; + public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); public static IEnumerable AvailableLanguages => TextPacks.Keys; @@ -31,54 +33,104 @@ namespace Barotrauma public static int LanguageVersion { get; private set; } = 0; - private static readonly ImmutableArray> CjkRanges = new[] - { - UnicodeRanges.HangulJamo, - UnicodeRanges.Hiragana, - UnicodeRanges.Katakana, - UnicodeRanges.CjkRadicalsSupplement, - UnicodeRanges.CjkSymbolsandPunctuation, - UnicodeRanges.EnclosedCjkLettersandMonths, - UnicodeRanges.CjkCompatibility, - UnicodeRanges.CjkUnifiedIdeographsExtensionA, - UnicodeRanges.CjkUnifiedIdeographs, - UnicodeRanges.HangulSyllables, - UnicodeRanges.CjkCompatibilityForms - }.Select(r => new Range(r.FirstCodePoint, r.FirstCodePoint+r.Length-1)) - .OrderBy(r => r.Start) - .ToImmutableArray(); + private static ImmutableArray> UnicodeToIntRanges(params UnicodeRange[] ranges) + => ranges + .Select(r => new Range(r.FirstCodePoint, r.FirstCodePoint + r.Length - 1)) + .OrderBy(r => r.Start) + .ToImmutableArray(); - /// - /// Does the string contain symbols from Chinese, Japanese or Korean languages - /// - public static bool IsCJK(LocalizedString text) + [Flags] + public enum SpeciallyHandledCharCategory { - return IsCJK(text.Value); + None = 0x0, + + CJK = 0x1, + Cyrillic = 0x2, + + All = 0x3 } - public static bool IsCJK(string text) - { - if (string.IsNullOrEmpty(text)) { return false; } + public static readonly ImmutableArray SpeciallyHandledCharCategories + = Enum.GetValues() + .Where(c => c is not (SpeciallyHandledCharCategory.None or SpeciallyHandledCharCategory.All)) + .ToImmutableArray(); + + private static readonly ImmutableDictionary>> SpeciallyHandledCharacterRanges + = new[] + { + (SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges( + UnicodeRanges.HangulJamo, + UnicodeRanges.Hiragana, + UnicodeRanges.Katakana, + UnicodeRanges.CjkRadicalsSupplement, + UnicodeRanges.CjkSymbolsandPunctuation, + UnicodeRanges.EnclosedCjkLettersandMonths, + UnicodeRanges.CjkCompatibility, + UnicodeRanges.CjkUnifiedIdeographsExtensionA, + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.HangulSyllables, + UnicodeRanges.CjkCompatibilityForms + )), + (SpeciallyHandledCharCategory.Cyrillic, UnicodeToIntRanges( + UnicodeRanges.Cyrillic, + UnicodeRanges.CyrillicSupplement, + UnicodeRanges.CyrillicExtendedA, + UnicodeRanges.CyrillicExtendedB, + UnicodeRanges.CyrillicExtendedC + )) + }.ToImmutableDictionary(); + public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(LocalizedString text) + => GetSpeciallyHandledCategories(text.Value); + + public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string text) + { + if (string.IsNullOrEmpty(text)) { return SpeciallyHandledCharCategory.None; } + + var retVal = SpeciallyHandledCharCategory.None; for (int i = 0; i < text.Length; i++) { char chr = text[i]; - for (int j = 0; j < CjkRanges.Length; j++) + + foreach (var category in SpeciallyHandledCharCategories) { - var range = CjkRanges[j]; + if (retVal.HasFlag(category)) { continue; } + + for (int j = 0; j < SpeciallyHandledCharacterRanges[category].Length; j++) + { + var range = SpeciallyHandledCharacterRanges[category][j]; - // If chr < range.Start, we know that it can't - // be in any of the following ranges, so let's - // not even bother checking them - if (chr < range.Start) { break; } + // If chr < range.Start, we know that it can't + // be in any of the following ranges, so let's + // not even bother checking them + if (chr < range.Start) { break; } - // This character is in a range, return true - if (range.Contains(chr)) { return true; } + // This character is in a range, set the flag + if (range.Contains(chr)) + { + retVal |= category; + break; + } + } + } + + if (retVal == SpeciallyHandledCharCategory.All) + { + // Input contains characters from all + // specially handled categories, there's + // no need to inspect the string further + return SpeciallyHandledCharCategory.All; } } - return false; + return retVal; } + public static bool IsCJK(LocalizedString text) + => IsCJK(text.Value); + + public static bool IsCJK(string text) + => GetSpeciallyHandledCategories(text).HasFlag(SpeciallyHandledCharCategory.CJK); + /// /// Check if the currently selected language is available, and switch to English if not /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 095b1bd38..84b97b6b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -108,6 +108,8 @@ namespace Barotrauma public readonly bool IsWallUpgrade; public readonly LocalizedString Name; + private readonly object mutex = new object(); + public readonly IEnumerable ItemTags; public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) @@ -119,7 +121,6 @@ namespace Barotrauma ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); - if (!nameIdentifier.IsEmpty) { Name = TextManager.Get($"{nameIdentifier}"); @@ -132,10 +133,13 @@ namespace Barotrauma public void DeterminePrefabsThatAllowUpgrades() { - prefabsThatAllowUpgrades.Clear(); - prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs - .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) - .Select(it => it.Identifier)); + lock (mutex) + { + prefabsThatAllowUpgrades.Clear(); + prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs + .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) + .Select(it => it.Identifier)); + } } public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab) @@ -153,26 +157,11 @@ namespace Barotrauma if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } - return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); - } - - public bool CanBeApplied(XElement element, UpgradePrefab prefab) - { - if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; } - - Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); - if (identifier.IsEmpty) { return false; } - - ItemPrefab? item = ItemPrefab.Find(null, identifier); - if (item == null) { return false; } - - Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty()); - - if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; } - - return item.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag); + lock (mutex) + { + return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || + ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); + } } public static UpgradeCategory? Find(Identifier identifier) @@ -209,9 +198,17 @@ namespace Barotrauma { if (type is MaxLevelModType.Invalid) { return false; } + int subTier = sub.Tier; + if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) + { + int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0); + + subTier = Math.Max(modifier, subTier); + } + if (tierOrClass.TryGet(out int tier)) { - return sub.Tier == tier; + return subTier == tier; } if (tierOrClass.TryGet(out SubmarineClass subClass)) @@ -443,6 +440,12 @@ namespace Barotrauma if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } } + if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) + { + int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); + level += modifier; + } + return level; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs index 740abe381..8d863767f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs @@ -36,9 +36,7 @@ namespace Barotrauma public EitherT(T value) { Value = value; } public override string? ToString() - { - return Value.ToString(); - } + => $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(T).NameWithGenerics()})"; public override bool TryGet(out T t) { t = Value; return true; } public override bool TryGet(out U u) { u = default!; return false; } @@ -75,9 +73,7 @@ namespace Barotrauma public EitherU(U value) { Value = value; } public override string? ToString() - { - return Value.ToString(); - } + => $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(U).NameWithGenerics()})"; public override bool TryGet(out T t) { t = default!; return false; } public override bool TryGet(out U u) { u = Value; return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 688c284c5..872ba4426 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -104,6 +104,8 @@ namespace Barotrauma (float)Math.Floor(value / div) * div; } + public static int RoundToInt(float v) => (int)MathF.Round(v); + public static float RoundTowardsClosest(float value, float div) { return (float)Math.Round(value / div) * div; @@ -560,35 +562,45 @@ namespace Barotrauma public static double LineSegmentToPointDistanceSquared(Point lineA, Point lineB, Point point) { - double xDiff = lineB.X - lineA.X; - double yDiff = lineB.Y - lineA.Y; + return LineSegmentToPointDistanceSquared(lineA.X, lineA.Y, lineB.X, lineB.Y, point.X, point.Y); + } + + public static float LineSegmentToPointDistanceSquared(Vector2 lineA, Vector2 lineB, Vector2 point) + { + return (float)LineSegmentToPointDistanceSquared(lineA.X, lineA.Y, lineB.X, lineB.Y, point.X, point.Y); + } + + private static double LineSegmentToPointDistanceSquared(double line1X, double line1Y, double line2X, double line2Y, double pointX, double pointY) + { + double xDiff = line2X - line1X; + double yDiff = line2Y - line1Y; if (xDiff == 0 && yDiff == 0) { - double v1 = lineA.X - point.X; - double v2 = lineA.Y - point.Y; + double v1 = line1X - pointX; + double v2 = line1Y - pointY; return (v1 * v1) + (v2 * v2); } // Calculate the t that minimizes the distance. - double t = ((point.X - lineA.X) * xDiff + (point.Y - lineA.Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff); + double t = ((pointX - line1X) * xDiff + (pointY - line1Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff); // See if this represents one of the segment's // end points or a point in the middle. if (t < 0) { - xDiff = point.X - lineA.X; - yDiff = point.Y - lineA.Y; + xDiff = pointX - line1X; + yDiff = pointY - line1Y; } else if (t > 1) { - xDiff = point.X - lineB.X; - yDiff = point.Y - lineB.Y; + xDiff = pointX - line2X; + yDiff = pointY - line2Y; } else { - xDiff = point.X - (lineA.X + t * xDiff); - yDiff = point.Y - (lineA.Y + t * yDiff); + xDiff = pointX - (line1X + t * xDiff); + yDiff = pointY - (line1Y + t * yDiff); } return xDiff * xDiff + yDiff * yDiff; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs new file mode 100644 index 000000000..7467600c5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + [NetworkSerialize] + public readonly record struct NetCollection(ImmutableArray Array) : INetSerializableStruct, IEnumerable + { + public static readonly NetCollection Empty = new(ImmutableArray.Empty); + + public NetCollection(params T[] elements) : this(elements.ToImmutableArray()) { } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 3d19c61ab..9aff08c3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -63,5 +63,21 @@ namespace Barotrauma => !(a == b); public abstract override string ToString(); + + public static implicit operator Option(Option.UnspecifiedNone _) + => None(); + } + + public static class Option + { + public sealed class UnspecifiedNone + { + private UnspecifiedNone() { } + internal static readonly UnspecifiedNone Instance = new(); + } + + public static UnspecifiedNone None => UnspecifiedNone.Instance; + + public static Option Some(T value) => Option.Some(value); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 552ccdc7a..8b9396787 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -101,5 +101,14 @@ namespace Barotrauma return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); } + + public static string NameWithGenerics(this Type t) + { + if (!t.IsGenericType) { return t.Name; } + + string result = t.Name[..t.Name.IndexOf('`')]; + result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; + return result; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index bb1950e0d..d6cc6a92f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -1,4 +1,7 @@ #nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + namespace Barotrauma { public abstract class Result @@ -13,6 +16,14 @@ namespace Barotrauma public static Failure Failure(TError error) => new Failure(error); + + public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value); + public abstract bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value); + + public abstract override string? ToString(); + + public static (Func> Success, Func> Failure) GetFactoryMethods() + => (Success, Failure); } public sealed class Success : Result @@ -22,6 +33,21 @@ namespace Barotrauma public readonly T Value; public override bool IsSuccess => true; + public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value) + { + value = Value; + return true; + } + + public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value) + { + value = default; + return false; + } + + public override string ToString() + => $"Success<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Value})"; + public Success(T value) { Value = value; @@ -35,6 +61,21 @@ namespace Barotrauma public readonly TError Error; public override bool IsSuccess => false; + + public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value) + { + value = default; + return false; + } + + public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value) + { + value = Error; + return true; + } + + public override string ToString() + => $"Failure<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Error})"; public Failure(TError error) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 49b231dfe..af4033b2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -314,6 +314,19 @@ namespace Barotrauma.IO //TODO: validate recursion? System.IO.Directory.Delete(path, recursive); } + + public static bool TryDelete(string path, bool recursive = true) + { + try + { + Directory.Delete(path, recursive); + return true; + } + catch + { + return false; + } + } public static DateTime GetLastWriteTime(string path) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index d00e02271..55120383d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -18,7 +18,7 @@ namespace Barotrauma #if OSX //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac - public static string SaveFolder = Path.Combine( + public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", @@ -27,13 +27,13 @@ namespace Barotrauma #else //"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows //"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux - public static string SaveFolder = Path.Combine( + public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Daedalic Entertainment GmbH", "Barotrauma"); #endif - public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer"); + public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer"); public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded"); @@ -43,9 +43,9 @@ namespace Barotrauma public static string TempPath { #if SERVER - get { return Path.Combine(SaveFolder, "temp_server"); } + get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); } #else - get { return Path.Combine(SaveFolder, "temp"); } + get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); } #endif } @@ -198,7 +198,10 @@ namespace Barotrauma } //deleting a multiplayer save file -> also delete character data - if (Path.GetFullPath(Path.GetDirectoryName(filePath)).Equals(Path.GetFullPath(MultiplayerSaveFolder))) + var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath)); + + if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) || + fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer))) { string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath); if (File.Exists(characterDataSavePath)) @@ -215,35 +218,70 @@ namespace Barotrauma } } - public static string GetSavePath(SaveType saveType, string saveName) + public static string GetSaveFolder(SaveType saveType) { - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; - return Path.Combine(folder, saveName); + string folder = string.Empty; + + if (!string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath)) + { + folder = GameSettings.CurrentConfig.SavePath; + if (saveType == SaveType.Multiplayer) + { + folder = Path.Combine(folder, "Multiplayer"); + } + if (!Directory.Exists(folder)) + { + DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder..."); + try + { + Directory.CreateDirectory(folder); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e); + folder = string.Empty; + } + } + } + if (string.IsNullOrEmpty(folder)) + { + folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder; + } + return folder; } public static IReadOnlyList GetSaveFiles(SaveType saveType, bool includeInCompatible = true) { - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; - if (!Directory.Exists(folder)) + string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder; + if (!Directory.Exists(defaultFolder)) { - DebugConsole.Log("Save folder \"" + folder + " not found! Attempting to create a new folder..."); + DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder..."); try { - Directory.CreateDirectory(folder); + Directory.CreateDirectory(defaultFolder); } catch (Exception e) { - DebugConsole.ThrowError("Failed to create the folder \"" + folder + "\"!", e); + DebugConsole.ThrowError("Failed to create the folder \"" + defaultFolder + "\"!", e); } } - List files = Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList(); + List files = Directory.GetFiles(defaultFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList(); + + var folder = GetSaveFolder(saveType); + if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) + { + files.AddRange(Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); + } + string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder; if (Directory.Exists(legacyFolder)) { files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); } + files = files.Distinct().ToList(); + List saveInfos = new List(); foreach (string file in files) { @@ -305,7 +343,7 @@ namespace Barotrauma { fileName = ToolBox.RemoveInvalidFileNameChars(fileName); - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; + string folder = GetSaveFolder(saveType); if (fileName == "Save_Default") { fileName = TextManager.Get("SaveFile.DefaultName").Value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs new file mode 100644 index 000000000..63f652f57 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs @@ -0,0 +1,336 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Networking; + +/* + * What are segment tables for? + * + * Segment tables help make our networking packet reading code more robust by + * clearly stating where part of a message begins. Previously we would've done + * something like: + * + * msg.WriteByte(SegmentType.A); + * ... + * msg.WriteByte(SegmentType.B); + * ... + * msg.WriteByte(SegmentType.EndOfMessage); + * + * The problem with this design is that it's hard to debug when the writing and reading + * code do not align for whatever reason. INetSerializableStruct is an awesome way + * of avoiding that problem, but deploying it on a broad scale means rewriting most + * of the netcode. That isn't going to happen any time soon, so this exists as an easier + * way of increasing robustness. + * + * A segment table is laid out as follows: + * + * [TablePointer: UInt16] + * [Segment: arbitrary] + * ... + * [Segment: arbitrary] + * [NumberOfSegments: UInt16] + * [(Identifier, SegmentPointer): (T, UInt16)] + * ... + * [(Identifier, SegmentPointer): (T, UInt16)] + * + * A pointer in this context is an offset relative to the BitPosition where the TablePointer is written. + * + * It is used as follows: + * + * using (var segmentTable = SegmentTableWriter.StartWriting(outMsg)) + * { + * segmentTable.StartNewSegment(T.A); + * ... write segment to outMsg ... + * segmentTable.StartNewSegment(T.B); + * ... write segment to outMsg ... + * } + * peer.SendMessage(outMsg); + * + * ... + * + * SegmentTableReader.Read(inc, + * segmentDataReader: (segment, inc) => + * { + * switch (segment) + * { + * ... read segments ... + * } + * } + * } + * + * The advantages of this approach are: + * - If a message is truncated or corrupted near the end, it becomes far more obvious because the table + * would not be read properly and look like garbage when printed to the console. + * - If the reading and writing code for a segment disagree on something, issues will be isolated to that + * one segment. + * - The code no longer has to fiddle with padding and temporary buffers because the segment table is able + * to handle content that is not byte-aligned just fine. + * - Exception handling is far easier when using a segment table, when combined with a using statement + * any uncaught exception will result in the entire table being skipped, allowing the remainder of the + * message to still be read. + * - It's harder to make mistakes in the implementation of segments themselves with this approach. By using + * the SegmentTableWriter and SegmentTableReader types, you get a type-safe way of delimiting segments + * and it's harder to forget to finalize a packet. + */ + +[NetworkSerialize] +public readonly record struct Segment(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct; + +readonly ref struct SegmentTableWriter where T : struct +{ + private readonly IWriteMessage message; + private readonly List> segments; + public readonly int PointerLocation; + private SegmentTableWriter(IWriteMessage message, int pointerLocation) + { + this.message = message; + this.PointerLocation = pointerLocation; + this.segments = new List>(); + } + + public static SegmentTableWriter StartWriting(IWriteMessage msg) + { + var retVal = new SegmentTableWriter(msg, msg.BitPosition); + msg.WriteUInt16(0); //reserve space for the table pointer + return retVal; + } + + private void ThrowOnInvalidState() + { + if (segments.Count >= UInt16.MaxValue) + { + throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>"); + } + + if (message.BitPosition - PointerLocation > UInt16.MaxValue) + { + throw new OverflowException( + $"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)"); + } + } + + public void StartNewSegment(T value) + { + ThrowOnInvalidState(); + segments.Add(new Segment(value, (UInt16)(message.BitPosition-PointerLocation))); + } + + public void Dispose() + { + ThrowOnInvalidState(); + int tablePosition = message.BitPosition; + + //rewrite the table pointer now that we know where the table ends + message.BitPosition = PointerLocation; + message.WriteUInt16((UInt16)(tablePosition-PointerLocation)); + + //write the table + message.BitPosition = tablePosition; + message.WriteUInt16((UInt16)segments.Count); + foreach (var segment in segments) + { + message.WriteNetSerializableStruct(segment); + } + } +} + +readonly ref struct SegmentTableReader where T : struct +{ + private class SegmentReadMsg : IReadMessage + { + private readonly IReadMessage underlyingMsg; + private readonly IReadOnlyList> segments; + private readonly int segmentIndex; + private readonly int offset; + private readonly int lengthBits; + public SegmentReadMsg(IReadMessage underlyingMsg, IReadOnlyList> segments, int segmentIndex, int offset, int lengthBits) + { + this.underlyingMsg = underlyingMsg; + this.segments = segments; + this.segmentIndex = segmentIndex; + this.offset = offset; + this.lengthBits = lengthBits; + + if (offset + lengthBits >= underlyingMsg.LengthBits) + { + throw new Exception( + $"Segment table is corrupt, segment length is invalid: {offset} + {lengthBits} >= {underlyingMsg.LengthBits}"); + } + } + + private void Check() + { + if (BitPosition > lengthBits) + { + throw new Exception($"Tried to read too much data from segment."); + } + } + + private TRead Check(TRead v) + { + Check(); + return v; + } + + public bool ReadBoolean() => Check(underlyingMsg.ReadBoolean()); + + public void ReadPadBits() + { + Check(); underlyingMsg.ReadPadBits(); + } + + public byte ReadByte() => Check(underlyingMsg.ReadByte()); + + public byte PeekByte() => Check(underlyingMsg.PeekByte()); + + public ushort ReadUInt16() => Check(underlyingMsg.ReadUInt16()); + + public short ReadInt16() => Check(underlyingMsg.ReadInt16()); + + public uint ReadUInt32() => Check(underlyingMsg.ReadUInt32()); + + public int ReadInt32() => Check(underlyingMsg.ReadInt32()); + + public ulong ReadUInt64() => Check(underlyingMsg.ReadUInt64()); + + public long ReadInt64() => Check(underlyingMsg.ReadInt64()); + + public float ReadSingle() => Check(underlyingMsg.ReadSingle()); + + public double ReadDouble() => Check(underlyingMsg.ReadDouble()); + + public uint ReadVariableUInt32() => Check(underlyingMsg.ReadVariableUInt32()); + + public string ReadString() => Check(underlyingMsg.ReadString()); + + public Identifier ReadIdentifier() => Check(underlyingMsg.ReadIdentifier()); + + public Color ReadColorR8G8B8() => Check(underlyingMsg.ReadColorR8G8B8()); + + public Color ReadColorR8G8B8A8() => Check(underlyingMsg.ReadColorR8G8B8A8()); + + public int ReadRangedInteger(int min, int max) => Check(underlyingMsg.ReadRangedInteger(min, max)); + + public float ReadRangedSingle(float min, float max, int bitCount) => Check(underlyingMsg.ReadRangedSingle(min, max, bitCount)); + + public byte[] ReadBytes(int numberOfBytes) => Check(underlyingMsg.ReadBytes(numberOfBytes)); + + public int BitPosition + { + get => underlyingMsg.BitPosition - offset; + set => Check(underlyingMsg.BitPosition = value + offset); + } + + public int BytePosition => BitPosition / 8; + + public byte[] Buffer => underlyingMsg.Buffer; + + public int LengthBits + { + get => lengthBits; + set => throw new InvalidOperationException($"Cannot resize {nameof(SegmentReadMsg)}"); + } + + public int LengthBytes => lengthBits / 8; + + public NetworkConnection Sender => underlyingMsg.Sender; + } + + private readonly IReadMessage message; + private readonly List> segments; + private readonly int exitLocation; + public readonly int PointerLocation; + private SegmentTableReader(IReadMessage message, List> segments, int pointerLocation, int exitLocation) + { + this.message = message; + this.segments = segments; + this.PointerLocation = pointerLocation; + this.exitLocation = exitLocation; + } + + public IReadOnlyList> Segments => segments; + + public enum BreakSegmentReading + { + No, + Yes + } + + public delegate BreakSegmentReading SegmentDataReader( + T segmentHeader, + IReadMessage incMsg); + + public delegate void ExceptionHandler( + Segment segmentWithError, + Segment[] previousSegments, + Exception exceptionThrown); + + public static void Read( + IReadMessage msg, + SegmentDataReader segmentDataReader, + ExceptionHandler? exceptionHandler = null) + { + int pointerLocation = msg.BitPosition; + int tablePointer = msg.ReadUInt16(); + int tableLocation = pointerLocation + tablePointer; + + int returnPosition = msg.BitPosition; + + //read the table + var segments = new List>(); + msg.BitPosition = tableLocation; + int numSegments = msg.ReadUInt16(); + for (int i = 0; i < numSegments; i++) + { + segments.Add(INetSerializableStruct.Read>(msg)); + } + + //store the exit location and go back to the top + int exitLocation = msg.BitPosition; + msg.BitPosition = returnPosition; + using var segmentTable = new SegmentTableReader(msg, segments, pointerLocation, exitLocation); + + for (int i = 0; i < segmentTable.Segments.Count; i++) + { + var segment = segmentTable.Segments[i]; + msg.BitPosition = segmentTable.PointerLocation + segment.Pointer; + try + { + if (segmentDataReader(segment.Identifier, new SegmentReadMsg( + msg, + segments, + i, + offset: segmentTable.PointerLocation + segment.Pointer, + lengthBits: (i < segmentTable.Segments.Count - 1 ? segments[i + 1].Pointer : tablePointer) - + segment.Pointer)) + is BreakSegmentReading.Yes) + { + break; + } + } + catch (Exception e) + { + var prevSegments = segments.Take(i).ToArray(); + if (exceptionHandler is not null) + { + exceptionHandler(segment, prevSegments, e); + } + else + { + throw new Exception( + $"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." + + (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}." : ""), + e); + } + } + } + } + + public void Dispose() + { + message.BitPosition = exitLocation; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs index 1b4d14dda..022fcdb3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs @@ -14,5 +14,17 @@ namespace Barotrauma result = default; return false; } + + public static async Task WaitForLoadingScreen(this Task task) + { + var result = await task; +#if CLIENT + while (GameMain.Instance.LoadingScreenOpen) + { + await Task.Delay((int)(1000 * Timing.Step)); + } +#endif + return result; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 1f5b69a16..a7d065ed0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -3,9 +3,11 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; +using System.Net; using System.Reflection; using System.Security.Cryptography; using System.Text; @@ -101,7 +103,7 @@ namespace Barotrauma string startPath = directory ?? ""; - string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/'); + string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/'); if (originalFilename.Replace('\\', '/').StartsWith(saveFolder)) { //paths that lead to the save folder might have incorrect case, @@ -721,5 +723,132 @@ namespace Barotrauma { return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; } + + private static readonly ImmutableHashSet affectedCharacters = ImmutableHashSet.Create('%', '+', 'ï¼…'); + + /// + /// Extends % and + characters to color tags in talent name tooltips to make them look nicer. + /// This obviously does not work in languages like French where a non breaking space is used + /// so it's just a a bit extra for the languages it works with. + /// + /// + /// + public static string ExtendColorToPercentageSigns(string original) + { + const string colorEnd = "‖color:end‖", + colorStart = "‖color:"; + + const char definitionIndicator = '‖'; + + char[] chars = original.ToCharArray(); + + for (int i = 0; i < chars.Length; i++) + { + if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; } + + // look behind + if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator) + { + int offset = colorEnd.Length; + + if (MatchesSequence(i - offset, colorEnd, chars)) + { + // push the color end tag forwards until the character is within the tag + char prev = currentChar; + for (int k = i - offset; k <= i; k++) + { + if (!TryGetAt(k, chars, out c)) { continue; } + + chars[k] = prev; + prev = c; + } + continue; + } + } + + // look ahead + if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator) + { + if (!MatchesSequence(i + 1, colorStart, chars)) { continue; } + + int offset = FindNextDefinitionOffset(i, colorStart.Length, chars); + + // we probably reached the end of the string + if (offset > chars.Length) { continue; } + + // push the color start tag back until the character is within the tag + char prev = currentChar; + for (int k = i + offset; k >= i; k--) + { + if (!TryGetAt(k, chars, out c)) { continue; } + + chars[k] = prev; + prev = c; + } + + // skip needlessly checking this section again since we already know what's ahead + i += offset; + } + } + + static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars) + { + int offset = initialOffset; + while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; } + return offset; + } + + static bool MatchesSequence(int index, string sequence, char[] chars) + { + for (int i = 0; i < sequence.Length; i++) + { + if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; } + } + + return true; + } + + static bool TryGetAt(int i, char[] chars, out char c) + { + if (i >= 0 && i < chars.Length) + { + c = chars[i]; + return true; + } + + c = default; + return false; + } + + return new string(chars); + } + + public static bool StatIdentifierMatches(Identifier original, Identifier match) + { + if (original == match) { return true; } + return Matches(original, match) || Matches(match, original); + + static bool Matches(Identifier a, Identifier b) + { + for (int i = 0; i < b.Value.Length; i++) + { + if (i >= a.Value.Length) { return b[i] is '~'; } + if (!CharEquals(a[i], b[i])) { return false; } + } + return false; + } + + static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b); + } + + public static bool EquivalentTo(this IPEndPoint self, IPEndPoint other) + => self.Address.EquivalentTo(other.Address) && self.Port == other.Port; + + public static bool EquivalentTo(this IPAddress self, IPAddress other) + { + if (self.IsIPv4MappedToIPv6) { self = self.MapToIPv4(); } + if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); } + return self.Equals(other); + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3c57a94c3..9080a4d12 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -49,6 +49,229 @@ v0.19.12.0 - Fixed inability to join servers that have enabled multiple mods with identical content. - Fixed tandem fire not working if there's a character between you and the other character on a periscope. +--------------------------------------------------------------------------------------------------------- +v0.20.12.0 +--------------------------------------------------------------------------------------------------------- + +Talent overhaul: +- Redesigned and rebalanced talents: lots of new talents, changes and balancing to existing ones and replacing some of the more broken ones with new ones. +- Redesigned talent tree structure: there's now a selection of "generic" talents, and after you've unlocked 4 of them, you can choose a specialization talent tree. After completing a specialization tree, you can choose another one. +- Lots of new talent-related items and 2 new pets. +- Players can select talents for bots in multiplayer. +- Anyone can fabricate the items bots have unlocked using talents. + +Balance: +- Slightly adjusted values of handheld weapons. + - Power levels match cost better. + - Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls). + - Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though). + - Improved ammo availability for basic weapons. +- Made some weapons available later in game, to increase feeling of progression. +- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses. + - Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances. + - PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths. + - Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair. + - All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit. +- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. +- Chance of finding good/excellent/masterwork quality items in higher-difficulty levels. +- Plasma cutter is now much better at cutting. +- Rebalanced damage dealt by tools. Damage should be a bit higher overall. + +Tutorial improvements: +- A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost. +- Various fixes and improvements to the Basic and Role tutorials. +- Added popups when completing tutorial chapters that allow you to restart or continue and to return back to the menu. +- Added a reactor infographic designed to help new players better understand the reactor interface. It's accessible through a help button on the top right corner of the interface. +- Added in-game hints for the genetic system. + +Changes and additions: +- New weapons: Rifle, Heavy Machine Gun, Machine Pistol, Harpoon Coil-Rifle. +- Flashlight can now be attached on all the ranged weapons held with two hands. +- Limit which submarines are available in each outpost: high-tier subs become available as you get further in the campaign, and the submarine class selection depends on the type of the outpost. +- Added a slider to the fabricator that can be used to select how many items to fabricate. +- Added an option to hide enemy health bars. +- Items' damage modifiers are shown in store tooltips. +- Added a button for treating all characters in one go to the medical clinic. +- Breaches through the submarine's outer hull throws shrapnels that can cause minor damage to nearby characters, making monsters that can't get inside more of a threat to the crew (as opposed to just the submarine itself). +- Added a new honking scary random event to beacon stations. +- Added some particle, sound and light effects to water-sensitive materials and made them explode when they've been in water for 3 seconds, not immediately. +- Affliction descriptions change depending on the strength of the affliction, and whether you're treating someone else or yourself. +- Added a button for opening the Steam Workshop to all tabs of the workshop menu. +- Added tooltips that explain how the bot spawn modes work to the server lobby. +- Added various new loot items to different creatures. +- Large monsters (Abyss monsters, Moloch, Watcher) drop items upon death. +- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version. +- Made saline significantly less effective as a treatment for bloodloss to make blood packs more useful. +- Nerfed flak cannon's explosive ammo. +- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders: implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions. +- Allow putting medium items (e.g. storage container) in medical and toxic cabinets. +- Some changes to wrecked item sprites (replacing the old low-res pictures with modified versions of the normal items' sprites). +- SMG can now be crafted. +- Optimized the server lobby: there was an issue in the logic that updates the microphone icon that caused the game to check available audio devices every frame. +- Optimized status monitors: previously some parts of their UI were always updated regardless if anyone is viewing the UI. + +Multiplayer: +- Quality of the radio voice chat diminishes with distance (gradually fading into radio static), similar to the way as the quality of the text chat. +- Voice chat range also affects spectators (= spectators can't hear players talking at the other side of the level). +- Fixed speech impediments only affecting the text chat, not the voice chat. +- Fixed radio voice chat not working properly if the range of the radio is larger than 250 meters. Happened because characters' positions aren't synced if they're further away than 250 meters from the client. In practice, the quality/volume of the chat would stop diminishing after 250 meters, and then immediately cut off when outside the radio range. +- Fixed inability to connect to IPv4 servers when IPv6 is disabled. +- Fixed occasional crashes when shutting down a server (for example with the error messages "pipe is broken" or "ChildServerRelay readTask did not run to completion"). +- Fixed "no core packages in the list of mods the server has enabled" error when trying to join a server that's using a different version of the core package you have enabled. +- Fixed "Input contains duplicate packages" error still occuring if you try to join a server that has empty content packages when you don't have those packages yourself. +- Fixed networking errors when the connection to the server is momentarily lost and then re-established. +- Added a cooldown to client name changes to prevent using it for spamming. +- Fixed bans issued with the "banaddress" command using a client's Steam ID not working. +- Server visibility can be adjusted in the server lobby (instead of having to restart the server). +- The "respawnnow" console command forces a respawn even if there's less than the minimum amount of players waiting for a respawn. + +Bugfixes: +- Fixed candidate box not being visible when using the Chinese input method. +- Fixed switching characters interrupting outpost events (even if there's currently no dialog active). +- Fixed certain events preventing other events from triggering when half-finished (e.g. preventing you from unlocking missions when an event is still running and for example waiting for you to talk to some other NPC). +- Fixed bots sometimes firing in a random direction when they equip a weapon. Happened because the aim and shoot inputs could already be active when the bot switches from another item to the weapon (e.g. from underwater scooter to some gun). +- Fixed pirates sometimes being unable to operate multiple turrets at the same time (even if there's enough crew to operate multiple), and attempting to operate hardpoints. +- Fabricator chooses the available ingredient that's in the worst condition when there's multiple suitable ingredients available. +- Fixed Esc not closing the campaign interfaces (map, store, shipyard, etc) but opening the pause menu instead. +- Fixed piezo crystals no longer spawning in the Great Sea. +- Fixed characters falling off ladders when using aimable tools. +- Fixed currently selected mission being included in the mission count displayed on the campaign map (i.e. showing "1/2" when you're choosing a new destination at an empty location). +- Fixed inability to sit in cafeteria chairs. +- Fixed projectile impacts getting triggered by gravity spheres and other TriggerComponents (meaning you couldn't hit monsters near a gravity sphere). +- Fixed blood pack fabrication recipe outputting only one item. +- Fixed tutorial not progressing when inserting a welding fuel tank inside the welding tool straight away, rather than inventory first. +- Fixed PUCS not beeping when you're underwater without a tank if you're inside a hull that has oxygen in it. +- Fixed some issues in sonar AITargets which made monsters hear the sonar when they shouldn't: switching to passive would immediately make the current directional ping cover 360 degrees, and whether the ping was directional or not would actually depend on whether the previous ping was directional, not what the mode is now. +- Fixed items getting autofilled into non-interactable containers in wrecks and outposts. +- Fixes to ID card tag issues in wrecks (prevented accessing the secure containers with the ID cards looted from the corpses). +- Fixed verifying file integrity on Steam resetting the server settings file. +- Fixed crashing if you try to open an access-restricted directory in the file selection dialog. +- Fixed a typo in physicorium shell's damage config, causing it to not do bleeding damage. +- Fixed money gain/lose popups no longer showing in the campaign. +- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. Caused e.g. drunkenness and bloodloss to never fully go away, causing issues with some talent effects. +- Fixed bots always opening the door/hatch they're trying to repair. +- Fixed power indicator not rotating with batteries. +- Fixed lights on welding tools and plasma cutters emitting light the next round if the round ends while using them. +- Fixed Camel's airlock not draining fully. +- Fixed Berilia's bottom EDC not being wired to a supercapacitor and a loose wire between the flak cannon and the right supercapacitor. +- Fixed status effects targeting "NearbyCharacters" or "NearbyItems" being applied twice. Modders: if you used this, double the effects (e.g. damage) to get the same results as previously. +- Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor". +- Fixed speech impediment from the husk infection making the bots unable to register any new targets autonomously (= without orders). +- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore any new enemies when they first envounter them. +- Fixed the default aim assist being 50% instead of 5%. Fixed aim assist not resetting when the reset button is pressed on the settings window. +- Fixed other players not seeing the spray particles when someone uses a sprayer in multiplayer. +- Fixed ability to "fire" (just dropping the projectile) hardpoints that are connected to a periscope and loader. +- When throwing an item (such as a grenade), the whole throw animation is played before the item is actually launched. Prevents being able to throw items at a ridiculous rate by spamming the hotkeys and LMB. +- Fixed portable pump's per-sub limit not working if you attach them at a spot with no background wall. +- Fixed oxygenlow resistance not affecting the time it takes to die in an unconscious state. +- Fixed bloodloss resistance not affecting how fast bleeding causes bloodloss. +- Fixed numpad keys toggling the chat when the Chat key is bind to nothing. +- Fixed main menu sometimes appearing half obstructed when starting the game on Mac. +- Fixed husk appendage breaking if the character already has extra limbs from e.g. genes. +- Fixed blocked doorways in Alien_Entrance3. +- Fixed Cyrillic symbols not being visible in the server list's server info panel when playing in a language other than Russian. +- Fixed certain genetic effects (such as regeneration from Hammerhead Matriarch genes) not working properly when multiple characters have the same effect. +- Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel. +- Fixed loot sometimes spawning in vending machines' output slots. +- Fixed water level sometimes "flickering" up and down when water is leaking to a room from the left or right. +- Fixed resetting UI position doing nothing to equipped items' UIs (e.g. handheld status monitor). +- Fixed items equipped in the health interface slot being sellable. +- Fixed inconsistent view ranges of large turrets. +- Fixed SMG magazine shape being inconsistent with the shape of the mag well on the SMG sprite. +- Fixed character portrait and health bar buttons being clickable (despite being hidden) when the health interface is open. +- Attempt to fix occasional crashes due to location store being null when teleporting from location to another with console commands. +- Fixes to impact-sensitive items exploding at the start of the round (e.g. at the start of explosive transport missions or when purchasing explosives). +- Attempt to fix bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret. +- Fixed focus staying on the highlighted item/character indefinitely if you keep holding LMB, even if you're outside interaction range. +- Prevented spawning of genetic materials outside creature inventories when the inventory size was too small, by increasing inventory sizes. +- Fixed minerals still sometimes being placed outside the level in mineral missions. +- Yet another fix to cave tunnels sometimes being too narrow to pass through. +- Fixed "man and his raptor" outpost event giving 1000 marks in an incorrect branch of the dialog (the one where you immediately accept the NPC on board, instead of the one where the NPC says they'll pay you 1000 mk). +- Fixed cases of interaction texts for focused item (most notoriously, the planter) not being updated correctly. +- Fixed "snap to grid" causing door gaps to get misaligned. +- Fixed weird equipping behavior on fruit and paints, causing them to be equipped in both hands when trying to unequip. +- Fixed junction boxes not getting damaged by water since the power rework. +- Fixed opiate withdrawal only reducing down to 20%, but never fully healing by itself. +- Fixed engines reverting back to the non-damaged sprite when they're damaged badly enough that the sprite starts shaking. +- Fixed walls being set up incorrectly in vertical abandoned outpost hallway modules, causing them to stick out into the connected modules. +- Fixed bots being unable to fix Typhon 2's top docking hatch or the wall right next to it. +- Fixed crashing when applying upgrades to linked subs, and there's more than one linked sub. +- Fixed Research Station being in the "Outpost" subcategory in the sub editor. +- Fixed bots dropping medicine from PUCS when changing its oxygen tank. + +Modding: +- Added a button to the main menu that can be used to update all installed mods when there's updates available. +- Mods with errors can no longer be enabled. +- Removed most of the debug console error spam seen when launching the game or opening the settings menu when faulty mods are installed. +- Fixed mods failing to show up in the mods list at all when they have certain kinds of errors. +- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral: always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the requirements are matched. +- Fixed increasing an item's HealthMultiplier making the items appear damaged in existing subs/saves (e.g. if you doubled an item's maximum condition, the items would remain in the old maximum condition and appear 50% damaged). +- Fixed crashing if a talent is triggered when the character receives some affliction, and that talent applies the same affliction on the character. +- Fixed crashing if the ingredient of a fabrication recipe can't be found. +- Fixed inability to sync properties of ItemComponents that the item has multiple of (meaning that it was only possible to e.g. edit the light color of the item's first LightComponent if it has multiple). +- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses). +- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration). +- Added 'DontApplyToHands' property to Propulsion, preventing extra force applying to hands when the item is held in hands (instead applying only to the character's whole body). +- Added a skill requirement conditional for StatusEffect, example: to make a status effect occur only if the target has less than 35 weapon skill. +- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. E.g. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5 s. +- Fixed sound's frequency multiplier not working in many cases (status effects, specific item sounds like turret movement sounds). +- Made it possible to check if some value is null or not with PropertyConditionals (e.g. CurrentHull="eq null"). +- Added UseEnvironment.None to Propulsion component. +- Fixed the debug console command "head" causing the character to disappear. The command can be used for changing the appearance of the character at runtime. +- Status effects of type "OnUse" on projectiles now trigger when the projectile is launched. Previously it launched when the projectile hit the target. Use OnImpact (or OnSuccess/OnFailure) when you want something to happen when the projectile hits the target. +- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition off, because it'd override the affliction specific value. +- Fixed item's OnSpawn effects being applied twice. + +--------------------------------------------------------------------------------------------------------- +v0.19.14.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed submarine upgrades getting clamped to the maximum of that upgrade between rounds, disregarding class/tier bonuses. + +--------------------------------------------------------------------------------------------------------- +v0.19.13.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed "failed to find the end of the bit field after 100 reads" error when trying to join a server that has a large number of mods enabled. +- Fixed "Tandem Fire" talent causing a crash due to the changes in the previous version. + +--------------------------------------------------------------------------------------------------------- +v0.19.12.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed submarine upgrades getting lost if you switch to a lower-tier sub that can't have as many levels of upgrades as the current sub, and then back again. +- Fixed some monster events not being as common/uncommon as intended. In more technical terms (which may be of interest to modders): the commonness defined as an attribute of an EventSet did nothing, making the event default to a commonness of 1. The commonnesses defined for specific level types worked correctly. +- Fixed clients getting stuck in a non-functional lobby if they happen to disconnect or get kicked back to the lobby at a specific point when loading a new round. +- Fixed large turret hardpoint origin being off, causing turrets installed on a large hardpoint to be misaligned. +- Attempt to fix crashing when disconnecting from the server you're hosting. +- Fixed Ctrl+Shift+S shortcut (quicksave) not working in the sub editor. +- Fixed toolbelts and storage containers in old subs going inside toolbelts. +- Fixed submarine tier resetting to default when reopening the sub editor's save dialog. +- Fixed sub editor not taking filename case into account when saving an existing sub: if you'd try to save the file with a different filename case, it'd ask about overwriting the existing sub, but save it as a new file even if you opt to overwrite. +- Fixes to Herja room names (use Engineering, Gunnery compartment, etc. labels), add camera to the front, with a periscope for the captain. +- Fixed non-purchaseable talent items not being available as extra cargo. +- Sorted extra cargo alphabetically + added a filter box. +- Fixed taking items that spawned inside another item (e.g. tanks in a diving mask) from NPCs spawned by an event not counting as stealing. +- Fixed characters falling off ladders when using aimable tools. +- Fixed money gain/lose popups no longer showing in the campaign. +- Fixed inability to manage the campaign if there's no-one with permissions alive. Previously we allowed anyone to manage the campaign if there's no-one with permissions present in the server, but that's not enough, because the players with permissions can't end the round if they're dead. Now if there's no-one with permissions alive, anyone is allowed to manage the campaign. +- If Select and Deselect have been bound to the same key, the deselect input is ignored when interacting with another item than the selected one. Prevents e.g. falling off ladders when trying to open a hatch when both Select and Deselect have been bound to E. +- Made it possible to enter a hostname (e.g. someserver.com) in the direct join prompt. +- Adjusted the size of the submarine list elements in the server lobby to reduce the amount of empty space on large resolutions. +- Fixed event texts for the "scan ruin" mission being in an incorrect language. +- Attachable items cannot be attached inside walls. +- Fixed distance at which you can attach items being slightly longer than the interact distance, making it possible to attach items out of reach. +- Fixed inability to turn when you're dual wielding melee weapons and attacking continuously. +- Fixed inability to cancel deconstruction if there's non-deconstructible items in the queue. +- Fixed local copy of a mod you're publishing not using the version number you've entered in the publish menu. +- Fixed crashing when trying to open the tab menu's character tab with a character who has no personality trait (may happen e.g. if you use a mod that adds custom personality traits and try to play that save without the mod). +- Fixed sourcerect issue in alien generator + decorative sprite not disappearing when the fuel rod is taken out. +- Fixed corrupted mods causing a nullref exception when autodetecting required mods in the sub editor. +- Fixed minerals not disappearing from mineral scanner if they get detached by something else than a character picking them up (e.g. by the destructible ice wall they're on breaking). +- Fixed event-specific metal crate deconstructing to steel. +- Fixed inability to join servers that have enabled multiple mods with identical content. +- Fixed tandem fire not working if there's a character between you and the other character on a periscope. + --------------------------------------------------------------------------------------------------------- v0.19.11.0 --------------------------------------------------------------------------------------------------------- @@ -124,7 +347,7 @@ Changes and additions: - Added a warning if a new keybind overlaps with any of the player's existing binds. - Overvoltage makes devices perform better, increasing the output of engines, making fabricators, deconstructors and pumps operate faster, electrical discharge coils do more damage, batteries recharge faster and oxygen generators generate more oxygen. Encourages operating the reactor manually and hopefully makes it a little more engaging. - Added more randomness to junction box overvoltage damage, and made partially damaged boxes take more damage from overvoltage. Prevents all boxes from breaking at the same time, making overvoltage less of a pain to deal with and intentionally overvolting devices more worthwhile. -- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate – a new benefit to operating reactors manually. +- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate � a new benefit to operating reactors manually. - Signals no longer set the fission and turbine rates of the reactor instantaneously, making automated reactor circuits less overpowered. They are still viable, but especially now with the addition of the extra incentives for operating the reactor manually, they're no longer as clearly the best and most efficient way to operate the reactor, making manual operation more worthwhile. - Made the "distort" camera effect a little less obtrusive and glitchy-looking (smoother texture + less heavy effect). - Made water-sensitive materials (lithium, potassium, sodium) spawn in waterproof chemical crates. @@ -404,7 +627,8 @@ Changes: - Handheld sonars can't detect minerals from inside the sub. - Changed the plus and minus button in the campaign settings into arrows. The button on the right increases difficulty, which in the case of the starting balance and supplies means reducing them, making the plus and minus buttons misleading. - Reduced costs of handheld weapon ammunition significantly. -- Slightly reduced effectiveness of harpoons and revolver round to compensate for the cheaper ammo. +- Up revolver & harpoon shop availability (especially at military outposts / armory merchants) +- Slightly reduced effectiveness of harpoons and revolver round to compensate for the cheaper / more available ammo. - Changed recipes for Handcannon, Assault Rifle and Auto-Shotgun. Weapon crafting is more expensive, to compensate for cheaper ammo. - Adjusted numerous other recipes and price costs of materials. Previously little used materials (like tin) are now used more. - Partially reintroduced the "toggle inventory" keybind, now called "toggle entity list". Even though toggling the in-game inventory is no longer possible, the keybind can be used to change the hotkey for toggling the sub editor's entity list. diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index ed6c346cc..3f843afaa 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -58,6 +58,11 @@ + + + + + diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml deleted file mode 100644 index 482899fc2..000000000 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ /dev/null @@ -1,57 +0,0 @@ - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs new file mode 100644 index 000000000..ece129035 --- /dev/null +++ b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs @@ -0,0 +1,56 @@ +#nullable enable + +using System; +using Xunit; +using Barotrauma; +using FluentAssertions; +using FsCheck; + +namespace TestProject; + +public sealed class GenericToolBoxTests +{ + public class CustomGenerators + { + public static Arbitrary IdentifierPairGenerator() + { + return Arb.From(from Identifier first in Arb.Generate() + from Identifier second in Arb.Generate().Where(second => second != first) + select new DifferentIdentifierPair(first, second)); + } + } + + public readonly struct DifferentIdentifierPair + { + public readonly Identifier First, + Second; + + public DifferentIdentifierPair(Identifier first, Identifier second) + { + if (first == second) { throw new InvalidOperationException("Identifiers must be different"); } + + First = first; + Second = second; + } + } + + public GenericToolBoxTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void MatchesStatIdentifier() + { + Prop.ForAll(static pair => + { + ToolBox.StatIdentifierMatches(pair.First, $"{pair.First}~{pair.Second}".ToIdentifier()).Should().BeTrue(); + ToolBox.StatIdentifierMatches($"{pair.First}~{pair.Second}".ToIdentifier(), pair.First).Should().BeTrue(); + ToolBox.StatIdentifierMatches(pair.First, pair.First).Should().BeTrue(); + + ToolBox.StatIdentifierMatches(pair.First, $"{pair.Second}~{pair.First}".ToIdentifier()).Should().BeFalse(); + ToolBox.StatIdentifierMatches(pair.First, pair.Second).Should().BeFalse(); + }).VerboseCheckThrowOnFailure(); + } +} diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs index 1e499dac0..f0bd29a02 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructImplementationChecks.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using Barotrauma; +using Microsoft.Xna.Framework; using Xunit; namespace TestProject; @@ -27,12 +29,56 @@ public class INetSerializableStructImplementationChecks foreach (var type in types) { - var members = NetSerializableProperties.GetPropertiesAndFields(type); + var concreteType = type; + if (type.IsGenericType) + { + // Plug in some known good parameters to evaluate + // a concrete instance of this generic type + + var paramsConstraints = type.GetGenericArguments() + .Select(p => p.GetGenericParameterConstraints()) + .ToImmutableArray(); + + var chosenArgs = new Type[paramsConstraints.Length]; + + for (int i = 0; i < paramsConstraints.Length; i++) + { + var constraints = paramsConstraints[i]; + bool refTypeConstraint = constraints.Any(c + => c.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); + bool valueTypeConstraint = constraints.Any(c + => c.GenericParameterAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint)); + if (refTypeConstraint && valueTypeConstraint) + { + throw new Exception($"Type \"{type.Name}\" has invalid generic constraints"); + } + + int rngMin = refTypeConstraint ? 3 : 0; + int rngMax = valueTypeConstraint ? 3 : 6; + + chosenArgs[i] = Rand.Range(rngMin, rngMax) switch + { + 0 => typeof(Vector2), + 1 => typeof(Point), + 2 => typeof(int), + + 3 => typeof(string), + 4 => typeof(float[]), + 5 => typeof(int[]), + + var invalid => throw new Exception($"Broken RNG ranges in test, got {invalid}") + }; + } + + concreteType = type.MakeGenericType(chosenArgs); + } + + var members = NetSerializableProperties.GetPropertiesAndFields(concreteType); foreach (var member in members) { void checkType(Type typeBeingChecked) { - Assert.True(tryFindBehavior(typeBeingChecked, out _), $"{type}.{member.Name} of type {member.Type} is unsupported in {nameof(INetSerializableStruct)}"); + Assert.True(tryFindBehavior(typeBeingChecked, out _), $"{concreteType}.{member.Name} of type {member.Type} is unsupported in {nameof(INetSerializableStruct)}"); Type? nestedType = null; if (typeBeingChecked.IsGenericType) { diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs index 614b0b23b..6b36c44e1 100644 --- a/Barotrauma/BarotraumaTest/TestProject.cs +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -15,6 +15,12 @@ namespace TestProject select new Vector2(x, y)); } + public static Arbitrary IdentifierGenerator() + { + return Arb.From(from string value in Arb.Generate().Where(static s => s != null) + select new Identifier(value)); + } + public static Arbitrary ColorGenerator() { return Arb.From(from int r in Gen.Choose(0, 255) diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 059f3f071..21c51a991 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -69,77 +69,71 @@ namespace Steamworks /// The ID of the file you want to download /// An optional callback /// Allows you to send a message to cancel the download anywhere during the process - /// How often to call the progress function + /// How often to call the progress function /// true if downloaded and installed correctly - public static async Task DownloadAsync( PublishedFileId fileId, Action progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) + public static async Task DownloadAsync( + PublishedFileId fileId, + Action progress = null, + int millisecondsUpdateDelay = 60, + CancellationToken? ct = null) { var item = new Steamworks.Ugc.Item( fileId ); - if ( ct == default ) - ct = new CancellationTokenSource( TimeSpan.FromSeconds( 60 ) ).Token; + var cancellationToken = ct ?? new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; + async Task waitOrCancel() + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(millisecondsUpdateDelay); + } + progress?.Invoke( 0.0f ); - if ( Download( fileId, highPriority: true ) == false ) - return item.IsInstalled; + Result downloadStartResult = Result.None; - // Steam docs about Download: - // If the return value is true then register and wait - // for the Callback DownloadItemResult_t before calling - // GetItemInstallInfo or accessing the workshop item on disk. - - // Wait for DownloadItemResult_t + void onDownloadFinished(Result r, ulong id) { - Action onDownloadStarted = null; + if (id != item.Id) { return; } + downloadStartResult = r; + } + OnDownloadItemResult += onDownloadFinished; - try + if (!Download(fileId, highPriority: true)) { return item.IsInstalled; } + + await Task.Delay(500); + + try + { + while (true) { - var downloadStarted = false; - - onDownloadStarted = (r, id) => downloadStarted = true; - OnDownloadItemResult += onDownloadStarted; + cancellationToken.ThrowIfCancellationRequested(); - int iters = 0; - while ( downloadStarted == false ) + progress?.Invoke(item.DownloadAmount); + + if (downloadStartResult != Result.None) { - ct.ThrowIfCancellationRequested(); - - iters++; - if (iters >= 1000 / milisecondsUpdateDelay) - { - if (!item.IsDownloading && !item.IsInstalled) - { - //force download to start if it's not started - if ( Download( fileId, highPriority: true ) == false ) - return item.IsInstalled; - } - iters = 0; - } - await Task.Delay( milisecondsUpdateDelay ); + if (downloadStartResult != Result.OK) { return false; } + break; } - } - finally - { - OnDownloadItemResult -= onDownloadStarted; + + if (!item.IsDownloadPending && !item.IsDownloading) + { + if (item.IsInstalled) + { + break; + } + if (!Download(fileId, highPriority: true)) + { + return item.IsInstalled; + } + } + + await Task.Delay( millisecondsUpdateDelay ); } } - - progress?.Invoke( 0.2f ); - await Task.Delay( milisecondsUpdateDelay ); - - //Wait for downloading completion + finally { - while ( true ) - { - ct.ThrowIfCancellationRequested(); - - progress?.Invoke( 0.2f + item.DownloadAmount * 0.8f ); - - if ( !item.IsDownloading && item.IsInstalled ) - break; - - await Task.Delay( milisecondsUpdateDelay ); - } + OnDownloadItemResult -= onDownloadFinished; } progress?.Invoke( 1.0f ); diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 742cfbf83..6eb0a9776 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -224,7 +224,7 @@ namespace Steamworks.Ugc } } - private ItemState State => (ItemState) SteamUGC.Internal.GetItemState( Id ); + private ItemState State => (ItemState)(SteamUGC.Internal?.GetItemState( Id ) ?? 0); public static async Task GetAsync( PublishedFileId id, int maxageseconds = 60 * 30 ) { diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs index ec4d56407..3e73f5980 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs @@ -106,6 +106,16 @@ namespace Microsoft.Xna.Framework { /// This event is only supported on the Windows DirectX, Windows OpenGL and Linux platforms. /// public event EventHandler TextInput; + + /// + /// Used for displaying uncommitted IME text. + /// + public event EventHandler TextEditing; + + /// + /// Used for when a key is pressed, including modifiers. + /// + public event EventHandler KeyDown; #endif #endregion Events @@ -152,6 +162,16 @@ namespace Microsoft.Xna.Framework { { EventHelpers.Raise(this, TextInput, e); } + + protected void OnKeyDown(object sender, TextInputEventArgs e) + { + EventHelpers.Raise(this, KeyDown, e); + } + + protected void OnTextEditing(object sender, TextEditingEventArgs e) + { + EventHelpers.Raise(this, TextEditing, e); + } #endif protected internal abstract void SetSupportedOrientations (DisplayOrientation orientations); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj index 5f9dbc68a..41c2f71d8 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj @@ -71,6 +71,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj index dd5a32d33..a8b412890 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj @@ -71,6 +71,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj index 3ff6019b1..e6bc855bf 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj @@ -84,6 +84,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 5ee24f7f0..43f2d7f32 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -75,6 +75,8 @@ internal static class Sdl TextEditing = 0x302, TextInput = 0x303, + TextEditingExt = 0x305, + MouseMotion = 0x400, MouseButtonDown = 0x401, MouseButtonup = 0x402, @@ -139,6 +141,8 @@ internal static class Sdl [FieldOffset(0)] public Keyboard.TextEditingEvent Edit; [FieldOffset(0)] + public Keyboard.TextEditingExtEvent EditExt; + [FieldOffset(0)] public Keyboard.TextInputEvent Text; [FieldOffset(0)] public Mouse.WheelEvent Wheel; @@ -240,6 +244,22 @@ internal static class Sdl return pointer; } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate bool d_sdl_istextinputshown(); + public static d_sdl_istextinputshown SDL_IsTextInputShown = FuncLoader.LoadFunction(NativeLibrary, "SDL_IsTextInputShown"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_starttextinput(); + public static d_sdl_starttextinput SDL_StartTextInput = FuncLoader.LoadFunction(NativeLibrary, "SDL_StartTextInput"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_stoptextinput(); + public static d_sdl_stoptextinput SDL_StopTextInput = FuncLoader.LoadFunction(NativeLibrary, "SDL_StopTextInput"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_settextinputrect(ref Rectangle rect); + public static d_sdl_settextinputrect SDL_SetTextInputRect = FuncLoader.LoadFunction(NativeLibrary, "SDL_SetTextInputRect"); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void d_sdl_clearerror(); public static d_sdl_clearerror ClearError = FuncLoader.LoadFunction(NativeLibrary, "SDL_ClearError"); @@ -825,6 +845,17 @@ internal static class Sdl public int Length; } + [StructLayout(LayoutKind.Sequential)] + public unsafe struct TextEditingExtEvent + { + public EventType Type; + public uint Timestamp; + public uint WindowId; + public byte* Text; + public int Start; + public int Length; + } + [StructLayout(LayoutKind.Sequential)] public unsafe struct TextInputEvent { diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs index e18d8a4d4..6f3a4f18b 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -136,8 +137,7 @@ namespace Microsoft.Xna.Framework { var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); - if (!_keys.Contains(key)) - _keys.Add(key); + if (!_keys.Contains(key)) _keys.Add(key); //TODO: rethink all of this char character = (char)KeyboardUtil.ApplyModifiers(ev.Key.Keysym.Sym, ev.Key.Keysym.Mod); @@ -147,38 +147,44 @@ namespace Microsoft.Xna.Framework character = '\0'; } - if (char.IsControl(character) || - key == Keys.Left || - key == Keys.Right || - key == Keys.Up || - key == Keys.Down) - { - _view.CallTextInput(character, key); - } + _view.CallKeyDown(character, key); } else if (ev.Type == Sdl.EventType.KeyUp) { var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); _keys.Remove(key); } - else if (ev.Type == Sdl.EventType.TextInput) + else if (ev.Type == Sdl.EventType.TextEditing) { - int len = 0; - string text = String.Empty; + string text; unsafe { - while (Marshal.ReadByte ((IntPtr)ev.Text.Text, len) != 0) { - len++; - } - var buffer = new byte [len]; - Marshal.Copy ((IntPtr)ev.Text.Text, buffer, 0, len); - text = System.Text.Encoding.UTF8.GetString (buffer); + text = ReadString(ev.Edit.Text); } - if (text.Length == 0) - continue; - foreach (var c in text) + + _view.CallTextEditing(text, ev.Edit.Start, ev.Edit.Length); + } + else if (ev.Type == Sdl.EventType.TextEditingExt) + { + string text; + unsafe { - var key = KeyboardUtil.ToXna((int)c); + text = ReadString(ev.EditExt.Text); + Sdl.Free((IntPtr)ev.EditExt.Text); + } + + _view.CallTextEditing(text, ev.EditExt.Start, ev.EditExt.Length); + } + else if (ev.Type == Sdl.EventType.TextInput) + { + string text; + unsafe { text = ReadString(ev.Text.Text); } + + if (text.Length is 0) { continue; } + + foreach (char c in text) + { + var key = KeyboardUtil.ToXna(c); _view.CallTextInput(c, key); } } @@ -194,11 +200,22 @@ namespace Microsoft.Xna.Framework IsActive = false; else if (ev.Window.EventID == Sdl.Window.EventId.Moved) _view.Moved(); - else if (ev.Window.EventID == Sdl.Window.EventId.Close) - _isExiting++; + else if (ev.Window.EventID == Sdl.Window.EventId.Close) _isExiting++; } } } + + static unsafe string ReadString(byte* ptr) + { + int len = 0; + while (Marshal.ReadByte((IntPtr)ptr, len) != 0) + { + len++; + } + var buffer = new byte [len]; + Marshal.Copy((IntPtr)ptr, buffer, 0, len); + return Encoding.UTF8.GetString(buffer); + } } public override void StartRunLoop() diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs index 7d5a071ca..5da9dc065 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs @@ -113,6 +113,13 @@ namespace Microsoft.Xna.Framework Sdl.SetHint("SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", "0"); Sdl.SetHint("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1"); + /* + * By default SDL2 will hide IME popups since it probably assumes the game will implement their own suggestions box. + * We don't want that, so this hint will allow the system native IME popups to show up when typing in the game. + */ + Sdl.SetHint("SDL_IME_SHOW_UI", "1"); + Sdl.SetHint("SDL_IME_SUPPORT_EXTENDED_TEXT", "1"); + // when running NUnit tests entry assembly can be null if (Assembly.GetEntryAssembly() != null) { @@ -333,6 +340,16 @@ namespace Microsoft.Xna.Framework OnTextInput(this, new TextInputEventArgs(c, key)); } + public void CallKeyDown(char c, Keys key = Keys.None) + { + OnKeyDown(this, new TextInputEventArgs(c, key)); + } + + public void CallTextEditing(string text, int start, int length) + { + OnTextEditing(this, new TextEditingEventArgs(text, start, length)); + } + public void DropFile(string filePath) { OnFileDropped(new FileDropEventArgs(filePath)); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs new file mode 100644 index 000000000..cce9ccc66 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs @@ -0,0 +1,17 @@ +using System; +namespace Microsoft.Xna.Framework +{ + public sealed class TextEditingEventArgs : EventArgs + { + public readonly string Text; + public readonly int Start; + public readonly int Length; + + public TextEditingEventArgs(string text, int start, int length) + { + Text = text; + Start = start; + Length = length; + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs new file mode 100644 index 000000000..631097ac4 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs @@ -0,0 +1,34 @@ +#nullable enable + +namespace Microsoft.Xna.Framework +{ + public static class TextInput + { + public static bool IsTextInputShown() + { + return Sdl.SDL_IsTextInputShown(); + } + + public static void StartTextInput() + { + Sdl.SDL_StartTextInput(); + } + + public static void StopTextInput() + { + Sdl.SDL_StopTextInput(); + } + + public static void SetTextInputRect(Rectangle rectangle) + { + Sdl.Rectangle r = new Sdl.Rectangle + { + X = rectangle.X, + Y = rectangle.Y, + Width = rectangle.Width, + Height = rectangle.Height + }; + Sdl.SDL_SetTextInputRect(ref r); + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 old mode 100644 new mode 100755 index bdd69273a..72cda07f7 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 differ diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 old mode 100644 new mode 100755 index 1f9bf7bb4..33ae87596 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 differ diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x64/SDL2.dll b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x64/SDL2.dll index 6cf858caa..8c6230e6f 100644 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x64/SDL2.dll and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x64/SDL2.dll differ diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x86/SDL2.dll b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x86/SDL2.dll index a25b05a06..0de0af520 100644 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x86/SDL2.dll and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Windows/x86/SDL2.dll differ diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 7d8c2e248..fa6c27732 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.1.32319.34 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -44,7 +44,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxTest", "Barotrauma\Bar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{382DFA63-78FC-41AC-BA85-630960A56E5C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/MacSolution.sln b/MacSolution.sln index af967aaf9..27e3629ab 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.1.32319.34 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -44,7 +44,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacTest", "Barotrauma\Barot EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{40BDE83D-61D5-481C-A53E-E0F5B23881E2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/WindowsSolution.sln b/WindowsSolution.sln index 88ab69c22..e354bff44 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.1.32319.34 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -227,6 +227,12 @@ Global {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|Any CPU.Build.0 = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.Build.0 = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.ActiveCfg = Release|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.Build.0 = Release|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.ActiveCfg = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -246,6 +252,7 @@ Global {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} {C98FE0D0-BC7D-4806-B592-734B53016FD8} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6} = {DE36F45F-F09E-4719-B953-00D148F7722A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A}